Char RNN

( 참고 : “딥러닝을 이용한 자연어 처리 입문” (https://wikidocs.net/book/2155) )

Char : Character (글자)

  • 입출력의 단위를 앞서 다뤘던 단어 레벨(word-level)에서, 글자 레벨(character-level)로 변경!

지금까지 모든 데이터 하나는 ‘단어’ 레벨이었다.

( ex. “내가” “가장” “좋아하는” 이라는 3개의 단어 데이터가 인풋으로 들어갔을 때, “과일은”이라는 1개의 단어 데이터가 아웃풋을 냄 )

하지만, 이제는 그 단위가 ‘글자’ 단위로 바꾸어서 해보겠다.

( ex. “a”,”p”,’p”,”l”이 들어갔을 때, “e”를 예측 )




1. Import Dataset

import numpy as np
from tensorflow.keras.utils import to_categorical
f = open('gutenberg.txt', 'rb')
lines = []
for line in f:
    line = line.strip()
    line = line.lower()
    line = line.decode('ascii','ignore')
    if len(line) > 0:
        lines.append(line)

f.close()


데이터의 첫 5문장을 보면 다음과 같다.

lines[:5]
['project gutenbergs alices adventures in wonderland, by lewis carroll',
 'this ebook is for the use of anyone anywhere at no cost and with',
 'almost no restrictions whatsoever.  you may copy it, give it away or',
 're-use it under the terms of the project gutenberg license included',
 'with this ebook or online at www.gutenberg.org']


이 모든 문장들을 연결해서 하나의 긴 텍스트 문장으로 나타낸다.

text = ' ' .join(lines)
len(text)
158783


첫 100 ‘글자(character)` 를 보면 다음과 같다.

text[:100]
'project gutenbergs alices adventures in wonderland, by lewis carroll this ebook is for the use of an'


이 모든 text에 사용된 글자들(특수문자 및 공백도 포함)을 보면 다음과 같음을 확인할 수 있다.

char_vocab = sorted(list(set(text)))
char_vocab
[' ',
 '!',
 '#',
..
 'x',
 'y',
 'z']


총 사용된 글자는 55개이다

vocab_size = len(char_vocab)


이전에 ‘단어’ 단위의 RNN/LSTM에서는 단어와 인덱스를 매칭시켜주었다.

이번에는, 글자와 인덱스를 매칭시켜준다.

char2index = dict((c,i) for i,c in enumerate(char_vocab))


위에서 만들어진 딕셔너리(char2index)는 ‘key’에 글자가, ‘value’에 인덱스가 할당되어있다.

이번엔 이 둘을 바꾼 (index2char)을 생성한다. (key에 인덱스, value에 글자)

index2char = {}
for key, value in char2index.items():
    index2char[value] = key


우리는 약 158000 길이의 text 문자열로부터 sample을 만들 것이다.

이 긴 텍스트를 , 하나의 문장에 60개의 단어가 들어가게 끔 2646개의 문장으로 나누어준다.

seq_length = 60
n_samples = int(np.floor((len(text)-1) / seq_length))
print('문장 샘플 수 : ',n_samples)
문장 샘플 수 :  2646


단어 단위 예측에서는, 각 문장의 마지막 단어(인덱스)를 y로 하고 나머지는 전부 x로 했었다.

글자 단위 예측에서는 약간 다르다. ‘a’,’p’,’p’,’l’,을 넣으면, 이보다 한 글자씩 밀린 ‘p,’p’,’l’,’e’를 예측하는 식으로 진행이 된다.

train_X = []
train_y = []

for i in range(n_samples):
    X_sample = text[i*seq_length : (i+1)*seq_length]
    X_encoded = [char2index[c] for c in X_sample]
    train_X.append(X_encoded)
    
    y_sample = text[i*seq_length + 1: (i+1)*seq_length+1] # 한개만큼 shift
    y_encoded = [char2index[c] for c in y_sample]
    train_y.append(y_encoded)
print(train_X[0])
print(train_y[0])
[44, 46, 43, 38, 33, 31, 48, 0, 35, 49, 48, 33, 42, 30, 33, 46, 35, 47, 0, 29, 40, 37, 31, 33, 47, 0, 29, 32, 50, 33, 42, 48, 49, 46, 33, 47, 0, 37, 42, 0, 51, 43, 42, 32, 33, 46, 40, 29, 42, 32, 8, 0, 30, 53, 0, 40, 33, 51, 37, 47]
[46, 43, 38, 33, 31, 48, 0, 35, 49, 48, 33, 42, 30, 33, 46, 35, 47, 0, 29, 40, 37, 31, 33, 47, 0, 29, 32, 50, 33, 42, 48, 49, 46, 33, 47, 0, 37, 42, 0, 51, 43, 42, 32, 33, 46, 40, 29, 42, 32, 8, 0, 30, 53, 0, 40, 33, 51, 37, 47, 0]


각 글자들을 One-Hot Encoding해준다.

train_X = to_categorical(train_X)
train_y = to_categorical(train_y)

정리 : train_X & train_y의 크기는 2646 x 60 x 55

  • 샘플의 수(No. of samples)가 2646개
  • 입력 시퀀스의 길이(input_length)가 60
  • 각 벡터의 차원(input_dim)이 55

train데이터의 X를 보면, (55차원의) 60개의 단어로 이루어진 2646개의 단어가 있다는 것을 확인할 수 있다.

train_X.shape
(2646, 60, 55)


( 이후 부분은, 글자단위 RNN/LSTM과 거의 동일하다 )


2. Modeling

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

256개의 neuron을 가진 LSTM layer를 두개 놓았다.

또한, return_sequences=True를 한 이유는, 각각의 state에서 예측 되는 값을 받기 위해서이다. 이것은 글자단위 RNN/LSTM과의 차이점이라고 할 수 있다.

또한, 마지막 layer를 보면 TimeDistributed가 Dense를 감싸고 있다. 이는 우리가 하는 예측이 ‘many-to-many’이기 때문에 필요한 것이다.

model = Sequential()
model.add(LSTM(256, input_shape=(None, train_X.shape[2]), return_sequences=True))
model.add(LSTM(256,return_sequences=True))
model.add(TimeDistributed(Dense(vocab_size, activation='softmax')))
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
lstm_2 (LSTM)                (None, None, 256)         319488    
_________________________________________________________________
lstm_3 (LSTM)                (None, None, 256)         525312    
_________________________________________________________________
time_distributed (TimeDistri (None, None, 55)          14135     
=================================================================
Total params: 858,935
Trainable params: 858,935
Non-trainable params: 0
_________________________________________________________________


그 이하 부분은 동일하다.

model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(train_X, train_y, epochs=20, 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],'로 예측을 시작!')
    X = np.zeros((1, length, vocab_size)) 

    for i in range(length):
        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)
sentence_generation(model, 50)