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에서 제공하고 있는 텍스트를 활용합니다. 여기서 사용하는 코드의 원본은 아래 링크를 기반으로 구성하였습니다.
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 구현에 대해 살펴보기 위해서는 다음 링크를 통해 확인해주세요)
여기에 손실함수 및 최적화를 위한 옵티마이저(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' ')
'Python > Data Analysis' 카테고리의 다른 글
[PyTorch] PyTorch를 활용한 텐서 생성하기 (0) | 2024.08.09 |
---|---|
[NLP] 임베딩과 Word2Vec 실습해보기 (0) | 2022.09.07 |
[NLP] N-gram 모델 구현하기 기초 | 임베딩, NLTK (0) | 2022.05.30 |
TensorFlow로 딥러닝 모델 구현하기 | 시퀀셜, 함수형, 서브클래싱 API (0) | 2022.05.17 |
[NLP] Python으로 영어 가독성 테스트하기 | Flesch, Gunning fog (0) | 2020.09.28 |