본문 바로가기

딥러닝/딥러닝을 이용한 자연어처리 입문

[딥러닝 NLP] 08. 순환신경망(SimpleRNN, LSTM, GRU, CharRNN)

피드포워드 신경망은 입력의 길이가 고정되어 있었다.

이번 장에서는 다양한 길이의 입력 시퀀스를 처리할 수 있는 RNN, 이를 개선한 LSTM, GRU에 대해 학습한다.

# 08-01 순환 신경망(Recurrent Neural Network, RNN)

1. RNN이란?

-시퀀스 모델: 입력과 출력을 시퀀스 단위로 처리. 따라서 입력벡터, 출력벡터, 은닉상태라는 표현 사용

-메모리셀/RNN셀(cell): RNN의 은닉층에서 이전값을 기억하는 메모리 역할을 수행하는 노드

-은닉상태(hidden state): 메모리셀이 출력층 혹은 다음시점인 t+1의 자신에게 보내는 값

 

RNN을 표현하는 두가지 방법(왼/오)

 

-FFNN과 달리 입출력길이를 다양하게 설계 가능 (일대다, 다대일, 다대다)

ex) 일대다: 이미지캡셔닝(하나의 이미지에 대해 다양한 제목 출력)

ex) 다대일: 감성분류/스팸메일분류

ex) 다대다:번역기, 개체명인식, 품사태깅

 

-재귀활동: 은닉층의 메모리셀은 각시점(t)에서 이전시점(t-1)으로부터 온 값을 자신의 입력으로 사용. 아래 그림들 참고

 

 

Step1) 은닉층: 현재시점의 입력값 Xt(d차원)과 가중치 Wx가 곱해지고, 이전시점으로부터의 은닉상태값 ht-1(D차원)과 가중치 Wh가 곱해지고, 편향 b가 더해져서 결과인 ht를 출력

Step2) 출력층: Step1에서 출력된 ht에 가중치 Wy가 곱해져 yt를 출력

 

* 가중지 Wx, Wh, Wy는 같은 층에서  동일한 값 공유, 은닉층 2개 이상일 경우 서로 다른 층 간에는 다름

 

2. Keras로 RNN 구현하기

(1) 입력값: (hidden_units, input_length, input_dim)

from tensorflow.keras.layers import SimpleRNN

# 기본 RNN 층 추가
model.add(SimpleRNN(hidden_units))
# 추가 인자 설정하는 법
model.add(SimpleRNN(hidden_units, input_shape=(timesteps, input_dim)))
# 또 다른 표기
model.add(SimpleRNN(hidden_units, input_length=M, input_dim=N))

 

hidden_units = 은닉상태의 크기(= 다음시점으로 내보낼 값의 크기, output_dim). 중소형모델은 보통 128, 256, 512, 1024

batch_size = 한번에 학습하는 데이터의 개수
timesteps(input_length)  = 시점의 수(문장의 길이)
input_dim = 입력의 크기(단어벡터의 차원)

RNN층은 (batch_size, timesteps, input_dim) 크기의 3D 텐서를 입력으로 받음

 

(2) 출력값: (batch_size, input_length, output_dim)

return_sequences=True/False(디폴트)

: True - 각 시점의 은닉상태를 모두 모아 전체시퀀스(3D 텐서)를 리턴(batch_size, input_length, output_dim) -> 다대다

: False - 메모리셀의 최종시점의 은닉상태(2D 텐서)를 리턴(batch_size, output_dim) -> 다대일

return_sequences=True / False 비교

 

* return_sequences=False(디폴트) 이고 batch_size를 정의하지 않았을 때

: Output Shape은 2D텐서 (batch_size, output_dim) 이므로 (None, 3)

model = Sequential()
model.add(SimpleRNN(3, input_shape=(2,10)))
# model.add(SimpleRNN(3, input_length=2, input_dim=10))와 동일함.
model.summary()

 

* return_sequences=False(디폴트) 이고 batch_size를 정의했을 때

: Output Shape은 2D텐서 (batch_size, output_dim) 이므로 (8, 3)

model = Sequential()
model.add(SimpleRNN(3, batch_input_shape=(8,2,10)))
model.summary()

 

* return_sequences=True 이고 batch_size를 정의했을 때

: Output Shape은 3D텐서 (batch_size, input_length, output_dim) 이므로 (8, 2, 3)

model = Sequential()
model.add(SimpleRNN(3, batch_input_shape=(8,2,10), return_sequences=True))
model.summary()

 

3. 파이썬으로 RNN 구현하기

Step1) 가상코드(pseudocode, 동장하지 않는 코드)로 긱본구조 살펴보기

hidden_state_t = 0 # t시점의 은닉상태
for input_t in input_length: # t시점의 입력값
    output_t = tanh(input_t, hidden_state_t) # t시점의 입력값과 t-1시점의 은닉상태를 입력으로 t시점의 은닉상태 계산
    hidden_state_t = output_t # 계산한 t시점의 은닉상태를 출력값으로 내보냄

 

Step2) 초기 은닉상태 출력

timesteps = 10
input_dim = 4
hidden_units = 8

# 입력에 해당되는 2D 텐서
inputs = np.random.random((timesteps, input_dim))

# 초기 은닉 상태는 0(벡터)로 초기화
hidden_state_t = np.zeros((hidden_units,)) 
print(hidden_state_t) # [0. 0. 0. 0. 0. 0. 0. 0.]

-> hidden_units(=output_dim)을 8로 설정했으므로 8차원의 0으로 구성된 벡터 출력

 

Step3) 가중치와 편향 출력

Wx = np.random.random((hidden_units, input_dim))  # 입력에 대한 가중치. (8, 4)크기의 2D텐서
Wh = np.random.random((hidden_units, hidden_units)) # 은닉상태에 대한 가중치. (8, 8)크기의 2D 텐서 
b = np.random.random((hidden_units,)) # 편향. (8,)크기의 1D텐서

print('가중치 Wx의 크기(shape) :',np.shape(Wx)) # (8,4)
print('가중치 Wh의 크기(shape) :',np.shape(Wh)) # (8,8)
print('편향의 크기(shape) :',np.shape(b)) # (8,)

 

Step4) 모든 시점의 은닉상태 출력

total_hidden_states = []

for input_t in inputs:
  # Wx * Xt + Wh * Ht-1 + b(bias)
  output_t = np.tanh(np.dot(Wx,input_t) + np.dot(Wh,hidden_state_t) + b)

  # 각 시점 t별 출력의 크기는 (timestep t, output_dim)
  # 각 시점의 은닉 상태의 값을 계속해서 누적
  total_hidden_states.append(list(output_t))
  hidden_state_t = output_t

# 출력 시 값을 깔끔하게 해주는 용도.
total_hidden_states = np.stack(total_hidden_states, axis = 0) 

# (timesteps, output_dim)
print(total_hidden_states)

 

4. 깊은 순환신경망(Deep RNN)

은닉층이 2개 이상인 RNN

은닉층을 2개 추가하는 코드

model = Sequential()
model.add(SimpleRNN(hidden_units, input_length=10, input_dim=5, return_sequences=True))
model.add(SimpleRNN(hidden_units, return_sequences=True))

첫번째 은닉층에서 return_sequences=True를 설정해 다음 은닉층으로 모든 시점의 은닉상태값을 보내줌

 

5. 양방향 순환신경먕(Bidirectional RNN)

-t시점에서 t-1시점(Forward States)뿐 아닌 t+1시점(Backward States)의 은닉상태값도 사용하는 것

-기본적으로 두 개의 메모리셀을 사용

주황셀은 앞시점의 은닉상태를, 초록셀은 뒤시점의 은닉상태를 전달받아 현재시점의 은닉상태를 계산

from tensorflow.keras.layers import Bidirectional

timesteps = 10
input_dim = 5

model = Sequential()
model.add(Bidirectional(SimpleRNN(hidden_units, return_sequences=True), 
				input_shape=(timesteps, input_dim)))

 

-양방향 RNN이면서 은닉층이 여러개인 깊은 양방향 순환신경망(Deep Bidirectional RNN) 가능

은닉층이 2개인 양방향 RNN

 

# 은닉층이 4개인 양방향 RNN 구현코드
model = Sequential()
model.add(Bidirectional(SimpleRNN(hidden_units, return_sequences=True), 
					input_shape=(timesteps, input_dim)))
model.add(Bidirectional(SimpleRNN(hidden_units, return_sequences=True)))
model.add(Bidirectional(SimpleRNN(hidden_units, return_sequences=True)))
model.add(Bidirectional(SimpleRNN(hidden_units, return_sequences=True)))

 

 

# 08-02 장단기 메모리(LSTM)

1. 바닐라 RNN의 한계

: 장기 의존성 문제, 시점이 길어질수록 앞의 정보가 뒤로 충분히 전달되지 못해, 비교적 짧은 시퀀스에 대해서만 효과적

 

2. 바닐라 RNN의 내부구조

ht = tanh( Wxxt + Whht-1 + b )

 

3. LSTM(Long Short-Term Memory)의 내부구조

전체적인 내부구조

은닉층의 메모리셀에 입력게이트, 망각게이트, 출력게이트를 추가해 불필요한 기억을 지우고 기억해야할 것만 남김

즉, 은닉상태 계산식이 복잡해졌고 셀 상태(cell state)라는 값이 추가됨(t시점의 셀 상태 = Ct)

 

(1) 입력게이트(현재시점 정보량 결정)

t시점의 입력값에 대한 게이트

i: 시그모이드 함수를 지나 0과 1사이 값 가짐

g: 하이퍼볼릭탄젠트 함수를 지나 -1과 1 사이 값 가짐

기억할 정보의 양  = 입력게이트에서 구한 i와 g에 원소별 곱을 진행한 것

*원소별 곱(extrywise product): 같은 위치의 성분끼리 곱하는 것

 

(2) 삭제게이트(이전시점 정보량 결정)

t-1시점의 셀상태값에 대한 게이트

f: 시그모이드 함수를 지나며 삭제 과정을 거쳐 0과 1 사이 값 가짐, 많이 삭제될수록 0, 온전할수록 1에 가까움

f 값을 가지고 셀 상태를 구함

 

(3) 셀 상태(총합 정보량 결정)

 

t시점의 셀상태=입력게이트에서 선택된 t시점의 기억 + 삭제게이트의 결과값인 t-1시점의 기억(f)-> t+1시점의 셀로 넘겨짐

f=0(삭제게이트 닫힘)이면 입력게이트 결과값만이 셀상태를 결정

i=0(입력게이트 닫힘)이면 삭제게이트 결과값만이 셀상태를 결정

 

(4) 출력게이트, 은닉상태

(3)에서 구한 셀상태(ct)는 tanh를 지나 -1과 1 사이 값이 됨(=tanh(ct))

t시점의 x와 t-1시점의 은닉상태는 시그모이드함수를 지나 0에서 1 사이 값이 됨(=출력게이트값, o)

o와 tanh(ct)에 원소별 곱을 진행한 값이 최종 출력게이트값 (ht)

 

# 08-03 게이트 순환 유닛(Gated Recurrent Unit, GRU)

-LSTM과는 달리 업데이트게이트, 리셋게이트의 2가지 게이트만이 존재

-성능은 LSTM과 유사하면서 보다 간단한 구조로 학습속도 향상

-데이터가 적을 때는 GRU, 많을때는 LSTM이 나음

 

-Keras로 GRU 구현 코드

model.add(GRU(hidden_size, input_shape=(timesteps, input_dim)))

 

 

# 08-04 Keras의 SimpleRNN, LSTM

1. 임의의 입력 생성

배치크기는 1, 4번의 시점(timesteps), 각 시점마다 5차원의 단어벡터가 입력으로 사용되는 예시 생성

import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import SimpleRNN, LSTM, Bidirectional

train_X = [[[0.1, 4.2, 1.5, 1.1, 2.8], [1.0, 3.1, 2.5, 0.7, 1.1], [0.3, 2.1, 1.5, 2.1, 0.1], [2.2, 1.4, 0.5, 0.9, 1.1]]]
train_X = np.array(train_X, dtype=np.float32)
print(train_X.shape) # (1, 4, 5)

 

2. SimpleRNN

return_sequences=True일 경우 모든시점의 은닉상태 값 출력(디폴트 False)

return_state=True일 경우 return_sequences의 여부와 상관없이 마지막시점의 은닉상태 값 출력(디폴트 False)

rnn = SimpleRNN(3, return_sequences=True, return_state=True)
hidden_states, last_state = rnn(train_X)

print(hidden_states.shape) # (1, 4, 3)
print(last_state.shape) # (1, 3)

 

-> 둘 다 True면 모든시점의 은닉상태, 마지막시점의 은닉상태라는 두 개의 출력 리턴

rnn = SimpleRNN(3, return_sequences=False, return_state=True)
hidden_state, last_state = rnn(train_X)

print(hidden_state.shape) # (1, 3)
print(last_state.shape) # (1, 3)

 

-> return_sequences=False이고 return_state=True면 마지막시점의 은닉상태 두 번 출력

 

3. LSTM

SimpleRNN과의 차이: return_state=True일 경우 마지막시점의 은닉상태뿐 아닌 셀상태까지 반환

lstm = LSTM(3, return_sequences=False, return_state=True)
hidden_state, last_state, last_cell_state = lstm(train_X)

print(hidden_state.shape) # (1, 3)
print(last_state.shape) # (1, 3)
print(last_cell_state.shape) # (1, 3)

 

4. Bidirectional LSTM

(1) return_sequences=False, return_state=True인 경우

정방향의 마지막, 역방향의 첫 은닉상태만 출력

bilstm = Bidirectional(LSTM(3, return_sequences=False, return_state=True, \
             kernel_initializer=k_init, bias_initializer=b_init, recurrent_initializer=r_init))
hidden_states, forward_h, forward_c, backward_h, backward_c = bilstm(train_X) # 5개의 값 반환

print(hidden_states.shape) # (1, 6)
print(forward_h.shape) # (1, 3)
print(backward_h.shape) # (1, 3)

 

-> return_sequences=False이므로 정방향 LSTM의 마지막시점의 은닉상태와 역방향 LSTM의 첫번째시점의 은닉상태가 연결된채 반환 (=hidden_states)

-> return_state=True이므로 정방향 LSTM의 은닉상태(=forward_h)와 셀상태(forward_c), 역방향 LSTM의 은닉상태(backward_h)와 셀상태(bacward_c)의 4가지를 반환

 

(2) return_sequences=True, return_state=True인 경우

정방향, 역방향 모두 첫번째는 첫번째끼리 출력

bilstm = Bidirectional(LSTM(3, return_sequences=True, return_state=True, \
             kernel_initializer=k_init, bias_initializer=b_init, recurrent_initializer=r_init))
hidden_states, forward_h, forward_c, backward_h, backward_c = bilstm(train_X)

-> return_sequences=True이므로 hidden states의 출력값에는 모든 시점의 은닉상태가 출력됨.

즉, 역방향 LSTM의 첫번째시점의 은닉상태는 정방향 LSTM의 첫번째시점의 은닉상태와 연결됨

 

# 08-05 RNN 언어모델(RNNLM)

1. RNNLM이란?

-RNN으로 만든 언어모델로, NNLM과 달리 입력의 길이를 고정하지 않아도 됨

-교사 강요(teacher forcing): t시점의 예측값이 아닌 t시점의 레이블(실제정답)을 t+1시점의 입력으로 사용하도록 강요하여 훈련시킴으로써, 잘못된 예측이 뒤의 예측까지 영향을 주지 못하도록 하는 효율적인 훈련방법. '훈련방법'이므로, '테스트 과정'에서는 원래대로 t시점의 출력(예측값)이 t+1시점의 입력으로 사용됨

ex) 'what will the fat cat sit on' 

-> 훈련: what will the fat cat sit 을 입력으로 넣으면 will the fat cat sit on 을 예측하도록 강요

[훈련] 출력층 활성화함수: 소프트맥스, 손실함수: 크로스엔트로피

 

-> 테스트: what 을 입력으로 will 을 예측, 이 will 을 다시 입력으로 the 를 예측, ...

                 즉, cat 은 what, will, the, fat 이라는 앞서 나온 시퀀스로 인해 결정된 단어

[테스트]

 

2. RNNLM의 구조

아래 그림과 같이 4개의 층으로 이루어져 있다.

입력층-임베딩층-은닉층-출력층

(1) 입력층

현재시점(timestep)=4 라고 할 때, 입력 x 는 4번째 입력단어인 fat 의 원핫벡터

 

(2) 임베딩층

입력받은 단어에 V(단어집합크기) X M(임베딩벡터크기) 임베딩행렬 E 를 곱해 임베딩벡터를 얻는 투사층

* 투사층(projection layer): NNLM에서 룩업테이블을 수행하는 층으로, 그 결과로 얻는 벡터가 임베딩벡터

임베딩층 수식

 

(3) 은닉층

현재시점의 입력인 임베딩벡터와 이전시점의 은닉상태인 ht-1 를 연산해 현재시점의 은닉상태 ht 계산

은닉층 수식

 

(4) 출력층

V차원의 벡터가 소프트맥스 함수를 지나 0과 1 사이의 실수값을 가지며 총합이 1인 벡터 yt 로 변환됨

출력층 수식

-손실함수로 크로스엔트로피를 사용해 역전파 수행(업데이트되는 가중치행렬: E, Wx, Wh, Wy 의 4개)

 

# 08-06 RNN으로 텍스트생성(Text Generation)

1. SimpleRNN 으로 텍스트생성

(1) 데이터 전처리

3개의 문장 ' 경마장에 있는 말이 뛰고 있다'  '그의 말이 법이다'  '가는 말이 고와야 오는 말이 곱다' 이 있을 때, 모델이 학습하게 될 데이터를 다음과 같이 재구성해보자.

 

# 정수 인코딩

import numpy as np
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical

text = """경마장에 있는 말이 뛰고 있다\n
그의 말이 법이다\n
가는 말이 고와야 오는 말이 곱다\n"""

tokenizer = Tokenizer()
tokenizer.fit_on_texts([text])
vocab_size = len(tokenizer.word_index) + 1 # 패딩을 고려해 1 더해줌

print(vocab_size) # 단어집합크기 V
print(tokenizer.word_index) # 각 단어에 부여된 정수인덱스
{'말이': 1, '경마장에': 2, '있는': 3, '뛰고': 4, '있다': 5, '그의': 6, '법이다': 7, '가는': 8, '고와야': 9, '오는': 10, '곱다': 11}

 

# 훈련 데이터 생성

# 문장 토큰화
sequences = list()
for line in text.split('\n'):
    encoded = tokenizer.texts_to_sequences([line])[0]
    for i in range(1, len(encoded)):
        sequence = encoded[:i+1]
        sequences.append(sequence)

print(len(sequences)) # 훈련 데이터 개수, 11

 

# 데이터 길이 일치시키기

# 가장 긴 샘플의 길이
max_len = max(len(l) for l in sequences) # 6
# 전체 샘플 길이 6으로 패딩
sequences = pad_sequences(sequences, maxlen=max_len, padding='pre')

 

# 각 데이터의 마지막 단어를 레이블로 지정

sequences = np.array(sequences)
X = sequences[:,:-1] # 마지막 이전까지의 값
y = sequences[:,-1] # 마지막 값(레이블)

 

# 레이블에 원핫인코딩 수행

y = to_categorical(y, num_classes=vocab_size)

 

(2) 모델 설계

 

# 모델 생성 및 학습

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Dense, SimpleRNN

embedding_dim = 10 # 임베딩벡터크기 M
hidden_units = 32 # 은닉상태크기

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim)) # 임베딩층 추가
model.add(SimpleRNN(hidden_units)) # 은닉층 추가
model.add(Dense(vocab_size, activation='softmax')) # 출력층 추가
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 다중클래스분류
model.fit(X, y, epochs=200, verbose=2) # 에포크 200 으로 학습

 

# 모델이 정확하게 예측하는지 확인하는 함수 정의

def sentence_generation(model, tokenizer, current_word, n):
    init_word = current_word # 현재단어
    sentence = ''

    # n번 반복
    for _ in range(n):
        # 현재단어에 대해 정수인코딩 및 패딩
        encoded = tokenizer.texts_to_sequences([current_word])[0] # 정수인코딩
        encoded = pad_sequences([encoded], maxlen=5, padding='pre') # 패딩
        
        # 예측단어를 result에 저장
        result = model.predict(encoded, verbose=0) # 예측
        # 만약 여기서 주어진 입력 다음에 나오는게 무엇인지 배운적 없다면 모델은 임의예측을 수행
        result = np.argmax(result, axis=1) # 저장
		
        # 갖고있는 단어(훈련시켜둔 단어들) 중에 예측단어(result)와 인덱스가 동일한 단어(word) 찾기
        for word, index in tokenizer.word_index.items(): 
            if index == result: # 만약 찾으면
                break # 그 단어(word)에서 멈춤(word가 result와 동일해짐)

        # 다음 입력값을 업데이트
        current_word = current_word + ' '  + word

        # 예측단어를 문장에 추가 (최종문장으로 출력하기 위해)
        sentence = sentence + ' ' + word

    sentence = init_word + sentence
    return sentence

 

# 사용해보기

print(sentence_generation(model, tokenizer, '경마장에', 4)) # 경마장에 있는 말이 뛰고 있다

 

'경마장에' 뒤에 4개의 단어가 있으므로, 4번보다 큰 숫자를 주면 모델은 임의예측을 수행

 

2. LSTM 으로 텍스트생성

LSTM을 사용하면 더 많은 데이터로 모델을 학습할 수 있다.

 

(1) 뉴욕타임즈 기사제목https://www.kaggle.com/aashita/nyt-comments )

 

# 데이터 확인

import pandas as pd
import numpy as np
from string import punctuation
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.utils import to_categorical

df = pd.read_csv('ArticlesApril2018.csv')
print(len(df.columns)) # 15
print(df.columns) # Index(['articleID', 'articleWordCount', 'byline', 'documentType', 
							'headline', 'keywords', 'multimedia', 'newDesk', 'printPage',
                            'pubDate', 'sectionName', 'snippet', 'source', 'typeOfMaterial',
                            'webURL'], dtype='object')
print(df['headline'].isnull().values.any()) # False (결측값 없음)

 

# 데이터 전처리

# 신문제목만 리스트로 저장
headline = []
headline.extend(list(df.headline.values)) 

# 'Unknown' 이라는 제목 삭제
headline = [word for word in headline if word != "Unknown"]

# 구두점제거, 소문자화
def repreprocessing(raw_sentence):
    preproceseed_sentence = raw_sentence.encode("utf8").decode("ascii",'ignore')
    return ''.join(word for word in preproceseed_sentence if word not in punctuation).lower()

preprocessed_headline = [repreprocessing(x) for x in headline]

 

(2) 훈련 데이터 생성

# 단어집합 생성
tokenizer = Tokenizer()
tokenizer.fit_on_texts(preprocessed_headline)
vocab_size = len(tokenizer.word_index) + 1 # 단어집합크기 V, 패딩 고려해 1 추가

# 문장 토큰화
sequences = list()
for sentence in preprocessed_headline:
    encoded = tokenizer.texts_to_sequences([sentence])[0]  # 정수인코딩
    for i in range(1, len(encoded)):
        sequence = encoded[:i+1]
        sequences.append(sequence)

# 패딩
# max_len = max(len(l) for l in sequences) 으로 최대문장길이 구함: 24
sequences = pad_sequences(sequences, maxlen=max_len, padding='pre') # 'pre': 앞을 0으로 패딩

# 맨 오른쪽 단어를 레이블로 분리
sequences = np.array(sequences)
X = sequences[:,:-1]
y = sequences[:,-1]

# 레이블에 원핫인코딩 수행
y = to_categorical(y, num_classes=vocab_size)

 

(3) 모델 설계

 

# 모델 생성 및 학습

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, Dense, LSTM

embedding_dim = 10 # 임베딩벡터크기 M
hidden_units = 128 # 은닉상태크기

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim)) # 임베딩층 추가
model.add(LSTM(hidden_units)) # LSTM 은닉층 추가
model.add(Dense(vocab_size, activation='softmax')) # 출력층 추가
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 다중클래스분류
model.fit(X, y, epochs=200, verbose=2) # 학습

 

# 모델 정확도 확인 함수 정의

def sentence_generation(model, tokenizer, current_word, n): # 모델, 토크나이저, 현재 단어, 반복할 횟수
    init_word = current_word
    sentence = ''

    # n번 반복
    for _ in range(n):
        encoded = tokenizer.texts_to_sequences([current_word])[0]
        encoded = pad_sequences([encoded], maxlen=max_len-1, padding='pre')

        # 예측단어를 result에 저장
        result = model.predict(encoded, verbose=0)
        result = np.argmax(result, axis=1)
        
        # 갖고있느 단어 중 예측단어와 같은 것에서 멈춤(=word)
        for word, index in tokenizer.word_index.items(): 
            if index == result:
                break

        # 다음 입력값 업데이트
        current_word = current_word + ' '  + word

        # 최종출력할 문장 업데이트
        sentence = sentence + ' ' + word

    sentence = init_word + sentence
    return sentence

 

# 사용해보기

print(sentence_generation(model, tokenizer, 'i', 10))
# i disapprove of school vouchers can i still apply for them

print(sentence_generation(model, tokenizer, 'how', 10))
# how to make facebook more accountable will so your neighbor chasing

 

 

# 08-07 문자단위 RNN (Char RNN)

지금까지 입력과 출력의 단위가 단어레벨이었다면, 이번에는 문자레벨로 RNN을 구현해보자.

단어단위가 아니기 때문에 임베딩층이 필요 없다.

 

1. Char RNNLM으로 다대일 LSTM 생성

(1) 데이터 전처리

데이터: 소설 이상한 나라의 앨리스 http://www.gutenberg.org/files/11/11-0.txt

import numpy as np
import urllib.request
from tensorflow.keras.utils import to_categorical

# 데이터 로드
urllib.request.urlretrieve("http://www.gutenberg.org/files/11/11-0.txt", filename="11-0.txt")

# 데이터 전처리
f = open('11-0.txt', 'rb')
sentences = []
for sentence in f:
    sentence = sentence.strip()
    sentence = sentence.lower()
    sentence = sentence.decode('ascii', 'ignore') # \xe2\x80\x99 등과 같은 바이트 열 제거
    if len(sentence) > 0:
        sentences.append(sentence)
f.close()

# 하나의 문자열로 붙여버리기(15만 9천자짜리 소설)
total_data = ' '.join(sentences)

# 문자집합 생성
char_vocab = sorted(list(set(total_data)))
vocab_size = len(char_vocab) # 56

 

# 추후 디코딩을 위한 작업

# 문자에 정수 부여
char_to_index = dict((char, index) for index, char in enumerate(char_vocab))

# 정수로 문자 찾는 딕셔너리 생성
index_to_char = {}
for key, value in char_to_index.items():
    index_to_char[value] = key

 

 

(2) 훈련 데이터 생성

seq_length = 60 # 한 샘플당 길이 설정
n_samples = int(np.floor((len(total_data) - 1) / seq_length)) # 샘플 개수

train_X = []
train_y = []

# 루프를 돌며 문장 추출(0:60 -> 60:120 -> 120:180 ...)
for i in range(n_samples):
    X_sample = total_data[i * seq_length: (i + 1) * seq_length] # 문장 추출
    X_encoded = [char_to_index[c] for c in X_sample] # 정수인코딩
    train_X.append(X_encoded)

    # 레이블 y = 다음에 올 문자(오른쪽으로 1칸)
    y_sample = total_data[i * seq_length + 1: (i + 1) * seq_length + 1]
    y_encoded = [char_to_index[c] for c in y_sample] # 정수인코딩
    train_y.append(y_encoded)
    
# 샘플출력 및 디코딩해보기
print(train_X[0]) # 첫번째 샘플
print(train_y[0]) # 첫번째 레이블
print([index_to_char[i] for i in train_X[0]]) # 첫번째 샘플 디코딩
print([index_to_char[i] for i in train_y[0]]) # 첫번째 레이블 디코딩

# 원핫인코딩
train_X = to_categorical(train_X) # 임베딩층을 사용하지 않으므로 X에도 원핫인코딩
train_y = to_categorical(train_y)
print(train_X.shape) # (2658, 60, 56)
print(train_y.shape) # (2658, 60, 56)

샘플수=2658개, input_length=60, input_dim=56(=원핫벡터의 차원)

 

(3) 모델 설계

# 모델 생성 및 학습

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, TimeDistributed

hidden_units = 256 # 은닉상태 크기

model = Sequential()
model.add(LSTM(hidden_units, input_shape=(None, train_X.shape[2]), return_sequences=True)) # 은닉층1
model.add(LSTM(hidden_units, return_sequences=True)) # 은닉층2
model.add(TimeDistributed(Dense(vocab_size, activation='softmax'))) # 전결합층

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 다중클래스분류
model.fit(train_X, train_y, epochs=80, verbose=2) # 모델 학습

 

# 문장 생성 함수 정의

def sentence_generation(model, length):
    # 문자에 대한 랜덤한 정수 생성
    ix = [np.random.randint(vocab_size)]

    # 랜덤한 정수로부터 맵핑되는 문자 생성
    y_char = [index_to_char[ix[-1]]]
    print(ix[-1],'번 문자',y_char[-1],'로 예측을 시작!')

    # (1, length, 55) 크기의 X 생성. 즉, LSTM의 입력 시퀀스 생성
    X = np.zeros((1, length, vocab_size))

    for i in range(length):
        # X[0][i][예측한 문자의 인덱스] = 1, 즉, 예측 문자를 다음 입력 시퀀스에 추가
        X[0][i][ix[-1]] = 1
        print(index_to_char[ix[-1]], end="")
        ix = np.argmax(model.predict(X[:, :i+1, :])[0], 1)
        y_char.append(index_to_char[ix[-1]])
    return ('').join(y_char)
result = sentence_generation(model, 100)
print(result)
# ury-men would have done just as well. the twelve jurors were to say in that dide. he went on in a di'

 

2. Char RNNLM 으로 다대다 LSTM 생성

(1) 데이터 전처리

import numpy as np
from tensorflow.keras.utils import to_categorical

raw_text = 임의의 텍스트
tokens = raw_text.split() # 단락구분 없애고
raw_text = ' '.join(tokens) # 한 문장으로 합침

# 중복을 제거한 문자 집합 생성
char_vocab = sorted(list(set(raw_text))) # 문자 집합
vocab_size = len(char_vocab) # 문자 집합의 크기

# 디코딩을 위한 정수인코딩
char_to_index = dict((char, index) for index, char in enumerate(char_vocab))

 

(2) 훈련데이터 생성

# 샘플 길이 설정 및 샘플추출
length = 11
sequences = []
for i in range(length, len(raw_text)):
    seq = raw_text[i-length:i] # 길이 11의 문자열을 지속적으로 만든다.
    sequences.append(seq)
print(len(sequences)) # 426개의 샘플

# 정수인코딩
encoded_sequences = []
for sequence in sequences: # 전체 데이터에서 문장 샘플을 1개씩 꺼내
    encoded_sequence = [char_to_index[char] for char in sequence] # 각 문자에 대해 정수인코딩
    encoded_sequences.append(encoded_sequence)

# 레이블 추출
encoded_sequences = np.array(encoded_sequences)
X_data = encoded_sequences[:,:-1] # 맨 마지막 위치의 문자를
y_data = encoded_sequences[:,-1] # 레이블로 저장

# 원핫잇코딩
X_data_one_hot = [to_categorical(encoded, num_classes=vocab_size) for encoded in X_data]
X_data_one_hot = np.array(X_data_one_hot)
y_data_one_hot = to_categorical(y_data, num_classes=vocab_size)
print(X_data_one_hot.shape) # (426, 10, 33)

 

(3) 모델 설계

# 모델 생성 및 학습

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM
from tensorflow.keras.preprocessing.sequence import pad_sequences

hidden_units = 64 # 은닉상태 크기

model = Sequential()
model.add(LSTM(hidden_units, input_shape=(X_data_one_hot.shape[1], X_data_one_hot.shape[2]))) # 은닉층
model.add(Dense(vocab_size, activation='softmax')) # 출력층 추가(전결합층)

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 다중클래스분류
model.fit(X_data_one_hot, y_data_one_hot, epochs=100, verbose=2) # 모델 학습

 

# 문장 생성 함수 정의

def sentence_generation(model, char_to_index, seq_length, seed_text, n):

    # 초기 시퀀스
    init_text = seed_text
    sentence = ''

    for _ in range(n):
        encoded = [char_to_index[char] for char in seed_text] # 정수인코딩
        encoded = pad_sequences([encoded], maxlen=seq_length, padding='pre') # 패딩
        encoded = to_categorical(encoded, num_classes=len(char_to_index))

        # 입력한 X(현재 시퀀스)에 대해서 y를 예측하고 y(예측한 문자)를 result에 저장.
        result = model.predict(encoded, verbose=0)
        result = np.argmax(result, axis=1)

        for char, index in char_to_index.items():
            if index == result:
                break

        # 다음 입력값 업데이트
        seed_text = seed_text + char

        # 최종 출력 문장 업데이트
        sentence = sentence + char

    # n번의 문자예측이 끝나면 최종 완성된 문장을 리턴
    sentence = init_text + sentence
    return sentence
print(sentence_generation(model, char_to_index, 10, 'I get on w', 80))
# I get on with life as a programmer, I like to hang out with programming and deep learning.