반응형

Char-RNN 개념

앞서 살펴봤던 통계기반 언어모델은 단어들의 빈도를 기반으로 학습을 합니다. 하지만 그렇기 때문에 발생하는 문제들이 있는데, N-gram을 다루면서 (링크) 모델의 한계를 살펴봤습니다.그렇다면 대안은 무엇일까요? 인공신경망에 대한 연구와 컴퓨팅 능력이 향상되면서 뉴럴 네트워크로 학습하는 언어모델도 있습니다. 여기서는 글자(Character) 단위 언어 모델인 Char-RNN에 대해 살펴보고자 합니다.

 

우선, RNN에 대해 생각해보겠습니다. RNN은 (t+1)에 대한 예측을 하기 위해 t까지 결과를 이용하여 순환적으로 예측에 사용합니다. 이는 다시 다음인 (t+2)에 영향을 미치는 과정을 끊임없이 반복하는 구조입니다.

그럼 이를 확장해서 문자단위 RNN 모델(Char-RNN)을 생각하게 보겠습니다. Char-RNN은 이전 시점의 예측문자를 다음 시점의 입력으로 사용하게 됩니다. 그렇기 때문에 학습을 위해서는 타겟을 입력값을 한 글자씩 뒤로 밀린 형태로 구성해주면 됩니다. 가령, "HELLO"를 학습시키고 싶은 경우 아래 표와 같이 구성이 될 것입니다.

입력 타겟
H E
E L
L L
L O

Char-RNN의 출력전체 문자 집합에 대한 softmax 출력값이 됩니다. 즉, 영문을 기준으로 할 때 26개의 알파벳에 대하여 확신의 정도를 나타내는 26 x 1의 행렬이 출력값이 되고, 가장 높은 확률의 문자를 다음에 올 글자로 예측하는 과정을 거칩니다.

 

다음은 구글에서 제공하고 있는 셰익스피어 데이터셋을 이용해 텍스트를 생성하는 간단한 프로젝트를 통해 Char-RNN을 구현하는 것을 실습해보고자 합니다. 내용이 꽤 길수 있으니 원하는 것만 찾고 싶으신 경우 옆의 TOC 스크롤에서 선택해 이동하시기 바랍니다. 순서는 아래와 같이 구현합니다.

 

(1) 준비 및 데이터 적재

(2) 데이터 전처리

(3) 모델링

(4) 훈련

(5) 텍스트 생성


(1) 준비 및 데이터 적재

기본적으로 필요한 라이브러리 및 데이터를 가져옵니다. 데이터는 Google에서 제공하고 있는 텍스트를 활용합니다. 여기서 사용하는 코드의 원본은 아래 링크를 기반으로 구성하였습니다.

Text Generation_Shakespeare

import tensorflow as tf
import numpy as np
import os
import time

data_dir = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
text = open(data_dir, 'rb').read()
vocab = sorted(set(text)) # unique characters

 

(2) 데이터 전처리

다음은 데이터 전처리 과정입니다. 우선 텍스트에서 문자 집합을 만든 후 인덱스를 부여합니다. 이렇게 부여된 인덱스를 기반으로 정수 인코딩을 실시합니다.

 

다음은 훈련 시퀀스를 생성합니다. 앞서 설명한 것처럼 훈련을 위한 입력값은 한칸씩 밀린 상태로 타겟데이터를 생성합니다. (split_input_target 함수)

# 문자열로 문자 집합 만들기 (토크나이징을 위한 딕셔너리)
char2idx = {char: ind for ind, char in enumerate(vocab)}
idx2char = np.array(vocab)

# 정수 인코딩
text_as_int = np.array([char2idx[c] for c in text])
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int) # train dataset 생성
sequences = char_dataset.batch(seq_length+1, drop_remainder=True) # 새로운 데이터셋 생성

# split_input_target 함수를 이용해서 input 데이터와 input 데이터를 한글자씩 뒤로 민 target 데이터를 생성합니다.
def split_input_target(chunk):
  input_text = chunk[:-1]
  target_text = chunk[1:]
  return input_text, target_text

dataset = sequences.map(split_input_target)

마지막으로 훈련데이터를 배치 형태로 가져옵니다.

# tf.data API를 이용해서 데이터를 섞고 batch 형태로 가져옵니다.
batch_size = 64
buffer_size = 10000
dataset = dataset.shuffle(10000).batch(batch_size, drop_remainder=True)

(3) 모델링

구현하고자 하는 Char-RNN은 아래와 같은 구조로 되어 있습니다. 이를 Subclassing을 활용해 구현합니다. (TensorFlow API 구현에 대해 살펴보기 위해서는 다음 링크를 통해 확인해주세요)

RNN 구조

여기에 손실함수 및 최적화를 위한 옵티마이저(Adam)까지 추가해 줍니다. 코드는 아래와 같이 구현하면 됩니다.

seq_length = 100     
batch_size = 64
embedding_dim = 256  
hidden_size = 1024   
num_epochs = 10
vocab_size = len(vocab)

# Char-RNN Model - Subclassing API
class RNN(tf.keras.Model):
 def __init__(self, batch_size):
   super(RNN, self).__init__()
   self.embedding_layer = tf.keras.layers.Embedding(vocab_size, embedding_dim,
                                                    batch_input_shape=[batch_size, None])
   self.hidden_layer_1 = tf.keras.layers.LSTM(hidden_size,
                                             return_sequences=True,
                                             stateful=True,
                                             recurrent_initializer='glorot_uniform')
   self.output_layer = tf.keras.layers.Dense(vocab_size)

 def call(self, x):
   embedded_input = self.embedding_layer(x)
   features = self.hidden_layer_1(embedded_input)
   logits = self.output_layer(features)

   return logits

# sparse cross-entropy loss function
def sparse_cross_entropy_loss(labels, logits):
  return tf.reduce_mean(tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True))

# 최적화를 위한 Adam 옵티마이저를 정의합니다.
optimizer = tf.keras.optimizers.Adam()

# 최적화를 위한 function을 정의합니다.
@tf.function
def train_step(model, input, target):
  with tf.GradientTape() as tape:
    logits = model(input)
    loss = sparse_cross_entropy_loss(target, logits)
  grads = tape.gradient(loss, model.trainable_variables)
  optimizer.apply_gradients(zip(grads, model.trainable_variables))

  return loss

 

(4) 훈련

훈련은 위에서 작성한 클래스를 활용해 컴파일(optimizer, loss function 설정) 후 fit 메서드를 통해 훈련해주는 것이 전부입니다. (물론 더 세부적으로 접근도 가능하지만, 여기서는 최대한 간단한 방법을 사용했습니다.) 

# 훈련
RNN_model = RNN(batch_size=batch_size)
RNN_model.compile(
    optimizer=optimizer,
    loss=sparse_cross_entropy_loss
)
history1 = RNN_model.fit(x=dataset, epochs= 40)

훈련이 잘 이뤄졌나를 확인하기 위해 손실값이 어떻게 변화하는지 살펴봤습니다. 에포크가 진행됨에 따라 잘 내려가고 있는 것을 확인할 수 있네요.

import matplotlib.pyplot as plt

def render_training_history(training_history):
    loss = training_history.history['loss']
    plt.title('Loss')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.plot(loss, label='Training set')
    plt.legend()
    plt.grid(linestyle='--', linewidth=1, alpha=0.5)
    plt.show()

render_training_history(history1)

(5) 텍스트 생성

이렇게 훈련한 모델을 바탕으로 텍스트를 생성하고자 합니다. 먼저 텍스트를 생성하나는 함수(generate_text)를 작성합니다. 그리고 앞서 훈련한 모델의 가중치를 가져와 적용해 샘플링을 해줍니다.

def generate_text(model, start_string):
  num_sampling = 1000  # 생성할 글자 수
  temperature = 1.0 # temperature 값에 따라 낮으면 정확한, 높으면 다양한 텍스트 생성

  # start_sting을 integer 형태로 변환(Vectorizing)
  input_indices = [char2idx[s] for s in start_string]
  input_indices = tf.expand_dims(input_indices, 0)

  text_generated = []
  model.reset_states()
  for i in range(num_sampling):
    predictions = model(input_indices)
    # 불필요한 batch dimension 삭제
    predictions = tf.squeeze(predictions, 0) 

    # 모델의 예측결과에 기반해서 랜덤 샘플링을 하기위해 categorical distribution을 사용
    predictions = predictions / temperature
    predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()

    # 예측된 character를 다음 input으로 사용
    input_indices = tf.expand_dims([predicted_id], 0)

    text_generated.append(idx2char[predicted_id])

  return (start_string + ''.join(text_generated))

sampling_RNN_model = RNN(batch_size = 1)
sampling_RNN_model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
sampling_RNN_model.build(tf.TensorShape([1, None]))

print(generate_text(sampling_RNN_model, start_string=u' ')

https://colab.research.google.com/github/trekhleb/machine-learning-experiments/blob/master/experiments/text_generation_shakespeare_rnn/text_generation_shakespeare_rnn.ipynb

반응형