순환 신경망을 활용한 문자열 생성

TensorFlow.org에서 보기 구글 코랩(Colab)에서 실행하기 깃허브(GitHub) 소스 보기

이 튜토리얼에서는 문자 기반 순환 신경망(RNN, Recurrent Neural Network)을 사용하여 어떻게 텍스트를 생성하는지 설명합니다. Andrej Karpathy의 순환 신경망의 뛰어난 효율에서 가져온 셰익스피어 데이터셋으로 작업할 것입니다. 이 데이터 셋에서 문자 시퀀스 ("Shakespear")가 주어지면, 시퀀스의 다음 문자("e")를 예측하는 모델을 훈련합니다. 모델을 반복하여 호출하면 더 긴 텍스트 시퀀스 생성이 가능합니다.

이 튜토리얼은 tf.keras즉시 실행(eager execution)을 활용하여 구현된 실행 가능한 코드가 포함되어 있습니다. 다음은 이 튜토리얼의 30번의 에포크(Epoch)로 훈련된 모델에서 "Q" 문자열로 시작될 때의 샘플 출력입니다.

QUEENE:
I had thought thou hadst a Roman; for the oracle,
Thus by All bids the man against the word,
Which are so weak of care, by old care done;
Your children were in your holy love,
And the precipitation through the bleeding throne.

BISHOP OF ELY:
Marry, and will, my lord, to weep in such a one were prettiest;
Yet now I was adopted heir
Of the world's lamentable day,
To watch the next way with his father with his face?

ESCALUS:
The cause why then we are all resolved more sons.

VOLUMNIA:
O, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, no, it is no sin it should be dead,
And love and pale as any will to that word.

QUEEN ELIZABETH:
But how long have I heard the soul for this world,
And show his hands of life be proved to stand.

PETRUCHIO:
I say he look'd on, if I must be content
To stay him from the fatal of our country's bliss.
His lordship pluck'd from this sentence then for prey,
And then let us twain, being the moon,
were she such a case as fills m

문장 중 일부는 문법적으로 맞지만 대부분 자연스럽지 않습니다. 이 모델은 단어의 의미를 학습하지는 않았지만, 고려해야 할 점으로:

  • 모델은 문자 기반입니다. 훈련이 시작되었을 때, 이 모델은 영어 단어의 철자를 모르거나 심지어 텍스트의 단위가 단어라는 것도 모릅니다.

  • 출력의 구조는 대본과 유사합니다. 즉, 텍스트 블록은 대개 화자의 이름으로 시작하고 이 이름들은 모든 데이터셋에서 대문자로 씌여 있습니다.

  • 아래에 설명된 것처럼 이 모델은 작은 텍스트 배치(각 100자)로 훈련되었으며 논리적인 구조를 가진 더 긴 텍스트 시퀀스를 생성할 수 있습니다.

설정

텐서플로와 다른 라이브러리 임포트

from __future__ import absolute_import, division, print_function, unicode_literals

!pip install -q tensorflow-gpu==2.0.0
import tensorflow as tf

import numpy as np
import os
import time

셰익스피어 데이터셋 다운로드

다음 코드를 실행하여 데이터를 불러오세요.

path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt
1122304/1115394 [==============================] - 0s 0us/step

데이터 읽기

먼저, 텍스트를 살펴봅시다:

# 읽은 다음 파이썬 2와 호환되도록 디코딩합니다.
text = open(path_to_file, 'rb').read().decode(encoding='utf-8')
# 텍스트의 길이는 그 안에 있는 문자의 수입니다.
print ('텍스트의 길이: {}자'.format(len(text)))
텍스트의 길이: 1115394자
# 텍스트의 처음 250자를 살펴봅니다
print(text[:250])
First Citizen:
Before we proceed any further, hear me speak.

All:
Speak, speak.

First Citizen:
You are all resolved rather to die than to famish?

All:
Resolved. resolved.

First Citizen:
First, you know Caius Marcius is chief enemy to the people.

# 파일의 고유 문자수를 출력합니다.
vocab = sorted(set(text))
print ('고유 문자수 {}개'.format(len(vocab)))
고유 문자수 65개

텍스트 처리

텍스트 벡터화

훈련 전, 문자들을 수치화할 필요가 있습니다. 두 개의 조회 테이블(lookup table)을 만듭니다: 하나는 문자를 숫자에 매핑하고 다른 하나는 숫자를 문자에 매핑하는 것입니다.

# 고유 문자에서 인덱스로 매핑 생성
char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)

text_as_int = np.array([char2idx[c] for c in text])

이제 각 문자에 대한 정수 표현을 만들었습니다. 문자를 0번 인덱스부터 고유 문자 길이까지 매핑한 것을 기억합시다.

print('{')
for char,_ in zip(char2idx, range(20)):
    print('  {:4s}: {:3d},'.format(repr(char), char2idx[char]))
print('  ...\n}')
{
  '\n':   0,
  ' ' :   1,
  '!' :   2,
  '$' :   3,
  '&' :   4,
  "'" :   5,
  ',' :   6,
  '-' :   7,
  '.' :   8,
  '3' :   9,
  ':' :  10,
  ';' :  11,
  '?' :  12,
  'A' :  13,
  'B' :  14,
  'C' :  15,
  'D' :  16,
  'E' :  17,
  'F' :  18,
  'G' :  19,
  ...
}
# 텍스트에서 처음 13개의 문자가 숫자로 어떻게 매핑되었는지를 보여줍니다
print ('{} ---- 문자들이 다음의 정수로 매핑되었습니다 ---- > {}'.format(repr(text[:13]), text_as_int[:13]))
'First Citizen' ---- 문자들이 다음의 정수로 매핑되었습니다 ---- > [18 47 56 57 58  1 15 47 58 47 64 43 52]

예측 과정

주어진 문자나 문자 시퀀스가 주어졌을 때, 다음 문자로 가장 가능성 있는 문자는 무엇일까요? 이는 모델을 훈련하여 수행할 작업입니다. 모델의 입력은 문자열 시퀀스가 될 것이고, 모델을 훈련시켜 출력을 예측합니다. 이 출력은 현재 타임 스텝(time step)의 다음 문자입니다.

RNN은 이전에 본 요소에 의존하는 내부 상태를 유지하고 있으므로, 이 순간까지 계산된 모든 문자를 감안할 때, 다음 문자는 무엇일까요?

훈련 샘플과 타깃 만들기

다음으로 텍스트를 샘플 시퀀스로 나눕니다. 각 입력 시퀀스에는 텍스트에서 나온 seq_length개의 문자가 포함될 것입니다.

각 입력 시퀀스에서, 해당 타깃은 한 문자를 오른쪽으로 이동한 것을 제외하고는 동일한 길이의 텍스트를 포함합니다.

따라서 텍스트를seq_length + 1개의 청크(chunk)로 나눕니다. 예를 들어, seq_length는 4이고 텍스트를 "Hello"이라고 가정해 봅시다. 입력 시퀀스는 "Hell"이고 타깃 시퀀스는 "ello"가 됩니다.

이렇게 하기 위해 먼저 tf.data.Dataset.from_tensor_slices 함수를 사용해 텍스트 벡터를 문자 인덱스의 스트림으로 변환합니다.

# 단일 입력에 대해 원하는 문장의 최대 길이
seq_length = 100
examples_per_epoch = len(text)//seq_length

# 훈련 샘플/타깃 만들기
char_dataset = tf.data.Dataset.from_tensor_slices(text_as_int)

for i in char_dataset.take(5):
  print(idx2char[i.numpy()])
F
i
r
s
t

batch 메서드는 이 개별 문자들을 원하는 크기의 시퀀스로 쉽게 변환할 수 있습니다.

sequences = char_dataset.batch(seq_length+1, drop_remainder=True)

for item in sequences.take(5):
  print(repr(''.join(idx2char[item.numpy()])))
'First Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou '
'are all resolved rather to die than to famish?\n\nAll:\nResolved. resolved.\n\nFirst Citizen:\nFirst, you k'
"now Caius Marcius is chief enemy to the people.\n\nAll:\nWe know't, we know't.\n\nFirst Citizen:\nLet us ki"
"ll him, and we'll have corn at our own price.\nIs't a verdict?\n\nAll:\nNo more talking on't; let it be d"
'one: away, away!\n\nSecond Citizen:\nOne word, good citizens.\n\nFirst Citizen:\nWe are accounted poor citi'

각 시퀀스에서, map 메서드를 사용해 각 배치에 간단한 함수를 적용하고 입력 텍스트와 타깃 텍스트를 복사 및 이동합니다:

def split_input_target(chunk):
    input_text = chunk[:-1]
    target_text = chunk[1:]
    return input_text, target_text

dataset = sequences.map(split_input_target)

첫 번째 샘플의 타깃 값을 출력합니다:

for input_example, target_example in  dataset.take(1):
  print ('입력 데이터: ', repr(''.join(idx2char[input_example.numpy()])))
  print ('타깃 데이터: ', repr(''.join(idx2char[target_example.numpy()])))
입력 데이터:  'First Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou'
타깃 데이터:  'irst Citizen:\nBefore we proceed any further, hear me speak.\n\nAll:\nSpeak, speak.\n\nFirst Citizen:\nYou '

이 벡터의 각 인덱스는 하나의 타임 스텝(time step)으로 처리됩니다. 타임 스텝 0의 입력으로 모델은 "F"의 인덱스를 받고 다음 문자로 "i"의 인덱스를 예측합니다. 다음 타임 스텝에서도 같은 일을 하지만 RNN은 현재 입력 문자 외에 이전 타임 스텝의 컨텍스트(context)를 고려합니다.

for i, (input_idx, target_idx) in enumerate(zip(input_example[:5], target_example[:5])):
    print("{:4d}단계".format(i))
    print("  입력: {} ({:s})".format(input_idx, repr(idx2char[input_idx])))
    print("  예상 출력: {} ({:s})".format(target_idx, repr(idx2char[target_idx])))
   0단계
  입력: 18 ('F')
  예상 출력: 47 ('i')
   1단계
  입력: 47 ('i')
  예상 출력: 56 ('r')
   2단계
  입력: 56 ('r')
  예상 출력: 57 ('s')
   3단계
  입력: 57 ('s')
  예상 출력: 58 ('t')
   4단계
  입력: 58 ('t')
  예상 출력: 1 (' ')

훈련 배치 생성

텍스트를 다루기 쉬운 시퀀스로 분리하기 위해 tf.data를 사용했습니다. 그러나 이 데이터를 모델에 넣기 전에 데이터를 섞은 후 배치를 만들어야 합니다.

# 배치 크기
BATCH_SIZE = 64

# 데이터셋을 섞을 버퍼 크기
# (TF 데이터는 무한한 시퀀스와 함께 작동이 가능하도록 설계되었으며,
# 따라서 전체 시퀀스를 메모리에 섞지 않습니다. 대신에,
# 요소를 섞는 버퍼를 유지합니다).
BUFFER_SIZE = 10000

dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)

dataset
<BatchDataset shapes: ((64, 100), (64, 100)), types: (tf.int64, tf.int64)>

모델 설계

모델을 정의하려면 tf.keras.Sequential을 사용합니다. 이 간단한 예제에서는 3개의 층을 사용하여 모델을 정의합니다:

  • tf.keras.layers.Embedding : 입력층. embedding_dim 차원 벡터에 각 문자의 정수 코드를 매핑하는 훈련 가능한 검색 테이블.
  • tf.keras.layers.GRU : 크기가 units = rnn_units인 RNN의 유형(여기서 LSTM층을 사용할 수도 있습니다.)
  • tf.keras.layers.Dense : 크기가 vocab_size인 출력을 생성하는 출력층.
# 문자로 된 어휘 사전의 크기
vocab_size = len(vocab)

# 임베딩 차원
embedding_dim = 256

# RNN 유닛(unit) 개수
rnn_units = 1024
def build_model(vocab_size, embedding_dim, rnn_units, batch_size):
  model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embedding_dim,
                              batch_input_shape=[batch_size, None]),
    tf.keras.layers.LSTM(rnn_units,
                        return_sequences=True,
                        stateful=True,
                        recurrent_initializer='glorot_uniform'),
    tf.keras.layers.Dense(vocab_size)
  ])
  return model
model = build_model(
  vocab_size = len(vocab),
  embedding_dim=embedding_dim,
  rnn_units=rnn_units,
  batch_size=BATCH_SIZE)

각 문자에 대해 모델은 임베딩을 검색하고, 임베딩을 입력으로 하여 GRU를 1개의 타임 스텝으로 실행하고, 완전연결층을 적용하여 다음 문자의 로그 가능도(log-likelihood)를 예측하는 로짓을 생성합니다:

모델을 통과하는 데이터의 사진

모델 사용

이제 모델을 실행하여 원하는대로 동작하는지 확인합니다.

먼저 출력의 형태를 확인합니다:

for input_example_batch, target_example_batch in dataset.take(1):
  example_batch_predictions = model(input_example_batch)
  print(example_batch_predictions.shape, "# (배치 크기, 시퀀스 길이, 어휘 사전 크기)")
(64, 100, 65) # (배치 크기, 시퀀스 길이, 어휘 사전 크기)

위 예제에서 입력의 시퀀스 길이는 100이지만 모델은 임의 길이의 입력에서 실행될 수 있습니다.

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (64, None, 256)           16640     
_________________________________________________________________
lstm (LSTM)                  (64, None, 1024)          5246976   
_________________________________________________________________
dense (Dense)                (64, None, 65)            66625     
=================================================================
Total params: 5,330,241
Trainable params: 5,330,241
Non-trainable params: 0
_________________________________________________________________

모델로부터 실제 예측을 얻으려면 출력 배열에서 샘플링하여 실제 문자 인덱스를 얻어야 합니다. 이 분포는 문자 어휘에 대한 로짓에 의해 정의됩니다.

배치의 첫 번째 샘플링을 시도해 봅시다:

sampled_indices = tf.random.categorical(example_batch_predictions[0], num_samples=1)
sampled_indices = tf.squeeze(sampled_indices,axis=-1).numpy()

이렇게 하면 각 타임 스텝(time step)에서 다음 문자 인덱스에 대한 예측을 제공합니다:

sampled_indices
array([36, 22, 24, 62, 28, 52, 36, 11, 27,  1, 29, 63, 36, 48, 60, 37, 22,
       60, 18,  8, 31, 51,  7,  3,  1,  7, 57, 14, 62,  9,  7, 41, 34,  2,
       15, 19, 40, 36, 36, 15, 54,  6, 32, 26, 43, 50, 51, 45, 39, 56,  7,
       42, 48, 41, 41, 17,  6, 31, 32, 13,  6, 24, 61, 26, 54,  9, 43, 54,
       10,  4, 30, 35, 54, 34, 12, 63, 17, 29, 40, 40, 38, 42, 63, 42, 63,
       27, 31, 11, 23, 12, 63, 63, 27, 58, 30, 17, 58, 14, 33,  1])

훈련되지 않은 모델에 의해 예측된 텍스트를 보기 위해 복호화합니다.

print("입력: \n", repr("".join(idx2char[input_example_batch[0]])))
print()
print("예측된 다음 문자: \n", repr("".join(idx2char[sampled_indices ])))
입력: 
 "ragon: he has wings; he's more than a\ncreeping thing.\n\nSICINIUS:\nHe loved his mother dearly.\n\nMENENI"

예측된 다음 문자: 
 'XJLxPnX;O QyXjvYJvF.Sm-$ -sBx3-cV!CGbXXCp,TNelmgar-djccE,STA,LwNp3ep:&RWpV?yEQbbZdydyOS;K?yyOtREtBU '

모델 훈련

이 문제는 표준 분류 문제로 취급될 수 있습니다. 이전 RNN 상태와 이번 타임 스텝(time step)의 입력으로 다음 문자의 클래스를 예측합니다.

옵티마이저 붙이기, 그리고 손실 함수

표준 tf.keras.losses.sparse_softmax_crossentropy 손실 함수는 이전 차원의 예측과 교차 적용되기 때문에 이 문제에 적합합니다.

이 모델은 로짓을 반환하기 때문에 from_logits 플래그를 설정해야 합니다.

def loss(labels, logits):
  return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)

example_batch_loss  = loss(target_example_batch, example_batch_predictions)
print("예측 배열 크기(shape): ", example_batch_predictions.shape, " # (배치 크기, 시퀀스 길이, 어휘 사전 크기")
print("스칼라 손실:          ", example_batch_loss.numpy().mean())
예측 배열 크기(shape):  (64, 100, 65)  # (배치 크기, 시퀀스 길이, 어휘 사전 크기
스칼라 손실:           4.1746216

tf.keras.Model.compile 메서드를 사용하여 훈련 절차를 설정합니다. 기본 매개변수의 tf.keras.optimizers.Adam과 손실 함수를 사용합니다.

model.compile(optimizer='adam', loss=loss)

체크포인트 구성

tf.keras.callbacks.ModelCheckpoint를 사용하여 훈련 중 체크포인트(checkpoint)가 저장되도록 합니다:

# 체크포인트가 저장될 디렉토리
checkpoint_dir = './training_checkpoints'
# 체크포인트 파일 이름
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")

checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(
    filepath=checkpoint_prefix,
    save_weights_only=True)

훈련 실행

훈련 시간이 너무 길지 않도록 모델을 훈련하는 데 10개의 에포크(Epoch)를 사용합니다. 코랩(Colab)에서는 런타임을 GPU로 설정하여 보다 빠르게 훈련할 수 있습니다.

EPOCHS=10
history = model.fit(dataset, epochs=EPOCHS, callbacks=[checkpoint_callback])
Epoch 1/10
172/172 [==============================] - 9s 50ms/step - loss: 2.5773
Epoch 2/10
172/172 [==============================] - 7s 39ms/step - loss: 1.8673
Epoch 3/10
172/172 [==============================] - 7s 39ms/step - loss: 1.6264
Epoch 4/10
172/172 [==============================] - 7s 39ms/step - loss: 1.4981
Epoch 5/10
172/172 [==============================] - 7s 38ms/step - loss: 1.4198
Epoch 6/10
172/172 [==============================] - 7s 39ms/step - loss: 1.3640
Epoch 7/10
172/172 [==============================] - 7s 39ms/step - loss: 1.3200
Epoch 8/10
172/172 [==============================] - 7s 39ms/step - loss: 1.2818
Epoch 9/10
172/172 [==============================] - 7s 39ms/step - loss: 1.2453
Epoch 10/10
172/172 [==============================] - 7s 39ms/step - loss: 1.2113

텍스트 생성

최근 체크포인트 복원

이 예측 단계를 간단히 유지하기 위해 배치 크기로 1을 사용합니다.

RNN 상태가 타임 스텝에서 타임 스텝으로 전달되는 방식이기 때문에 모델은 한 번 빌드된 고정 배치 크기만 허용합니다.

다른 배치 크기로 모델을 실행하려면 모델을 다시 빌드하고 체크포인트에서 가중치를 복원해야 합니다.

tf.train.latest_checkpoint(checkpoint_dir)
'./training_checkpoints/ckpt_10'
model = build_model(vocab_size, embedding_dim, rnn_units, batch_size=1)

model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))

model.build(tf.TensorShape([1, None]))
model.summary()
Model: "sequential_1"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (1, None, 256)            16640     
_________________________________________________________________
lstm_1 (LSTM)                (1, None, 1024)           5246976   
_________________________________________________________________
dense_1 (Dense)              (1, None, 65)             66625     
=================================================================
Total params: 5,330,241
Trainable params: 5,330,241
Non-trainable params: 0
_________________________________________________________________

예측 루프

다음 코드 블록은 텍스트를 생성합니다:

  • 시작 문자열 선택과 순환 신경망 상태를 초기화하고 생성할 문자 수를 설정하면 시작됩니다.

  • 시작 문자열과 순환 신경망 상태를 사용하여 다음 문자의 예측 배열을 가져옵니다.

  • 다음, 범주형 배열을 사용하여 예측된 문자의 인덱스를 계산합니다. 이 예측된 문자를 모델의 다음 입력으로 활용합니다.

  • 모델에 의해 리턴된 RNN 상태는 모델로 피드백되어 이제는 하나의 단어가 아닌 더 많은 컨텍스트를 갖추게 됩니다. 다음 단어를 예측한 후 수정된 RNN 상태가 다시 모델로 피드백되어 이전에 예측된 단어에서 더 많은 컨텍스트를 얻으면서 학습하는 방식입니다.

텍스트를 생성하기 위해 모델의 출력이 입력으로 피드백

생성된 텍스트를 보면 모델이 언제 대문자로 나타나고, 절을 만들고 셰익스피어와 유사한 어휘를 가져오는지 알 수 있습니다. 훈련 에포크(Epoch)가 적은 관계로 논리적인 문장을 형성하는 것은 아직 훈련되지 않았습니다.

def generate_text(model, start_string):
  # 평가 단계 (학습된 모델을 사용하여 텍스트 생성)

  # 생성할 문자의 수
  num_generate = 1000

  # 시작 문자열을 숫자로 변환(벡터화)
  input_eval = [char2idx[s] for s in start_string]
  input_eval = tf.expand_dims(input_eval, 0)

  # 결과를 저장할 빈 문자열
  text_generated = []

  # 온도가 낮으면 더 예측 가능한 텍스트가 됩니다.
  # 온도가 높으면 더 의외의 텍스트가 됩니다.
  # 최적의 세팅을 찾기 위한 실험
  temperature = 1.0

  # 여기에서 배치 크기 == 1
  model.reset_states()
  for i in range(num_generate):
      predictions = model(input_eval)
      # 배치 차원 제거
      predictions = tf.squeeze(predictions, 0)

      # 범주형 분포를 사용하여 모델에서 리턴한 단어 예측
      predictions = predictions / temperature
      predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()

      # 예측된 단어를 다음 입력으로 모델에 전달
      # 이전 은닉 상태와 함께
      input_eval = tf.expand_dims([predicted_id], 0)

      text_generated.append(idx2char[predicted_id])

  return (start_string + ''.join(text_generated))
print(generate_text(model, start_string=u"ROMEO: "))
ROMEO: DWIVIVGGZjVAZPVXDLUHJDzPXXGRX$3FHK&KKIEQXDXDWO$ZGVZXQK3XD3XzQXCBGKjDEXXC&KV$NXF,SqXQPKZKjEDDDKNXDZFZVzJRYQQv33QJDQGQXDVF.XPZDVVSZQXKXRWDKKxV3KGXGQXX:FQSYDXzEXDQVKQzQzZAZZX33Xx$&XBDVRDJVzHMjXDXjDVZQMjjDIYX3JZXQEDQXzDQQK$XX$z&ZEXVSEzZZRV,KDQXJ$DYFDX&X3QHDGXMjDKK:X$GKEDKKZGzXGX-DV3z&QjXQKEKYXXSXPqXGDxSFZzQjRTJZXYYQ$DXjVJVKZ&XVK!E$jVVF$XQIKqZ&QXWDX3RUKNVxAKJPxDDXq3XWVqDXz?XYEQXVFz&P&KGDZZj3FQJNDE3XVZUKKJX,RjXXzWKKEjZZ3ZqKQKQYDYYZZKX3VK?vVDDD&XDGJKQUZZ3ZKDJRFQXND;IJKHQQVYzDx&KQX3XFX$QXVHQX!DX&XDXKKxXFYjKZjXS3jz3QXGXKH$DKXGXxD&KZ3V3DXKEQHKDFAV3&xZzDZJSzGXgYXzJVZXYDVVDXXEQ3YGXVKEKFVQSzVxXDCZV3XE3KBWYDGX&UXXYzRNVQXjVXFXzDV3YDXNZjQ!ZZVQ$&XVXKAJKV3XxZj$D$$XXZzD&,XXKJPX&&XX&Z-XZzVXPj3Cx.?Z3DDGQXJQ;$VVK&GQXGDQKVQzxXKFN3QxKQz!DKAAFJX&zZYXVAZVzUQ&3jZZVQFK$XXJKKXK&&$QX&ZVXXxXxMR$KzYQX3XK3XVS&VDD3KDN&$ZjjCSEQ3XJxx3xYZxZ3GVD3&jZ3LDSZQXEQFXB$3FVMKQZMXR$JXoZDVKz&jDEvjQZVVzOJUFGBKXZX3KZEjjX&XZVzdJX&XD3QXKVjQKYjXQLKXjX$KZ&VKQX$DBN&&C-FQUKDXXXGjGKJKXYXV$IXz&3X33X$ZXLDXV3xENDQXZDZFDDKXVZFVEVYYDV3XjjPJ&&FHFM&

결과를 개선하는 가장 쉬운 방법은 더 오래 훈련하는 것입니다(EPOCHS = 30을 시도해 봅시다).

또한 다른 시작 문자열을 시험해 보거나 모델의 정확도를 높이기 위해 다른 RNN 레이어를 추가하거나 온도 파라미터를 조정하여 많은 혹은 적은 임의의 예측을 생성할 수 있습니다.

고급: 맞춤식 훈련

위의 훈련 절차는 간단하지만 많은 권한을 부여하지는 않습니다.

이제 수동으로 모델을 실행하는 방법을 살펴 보았으니 이제 훈련 루프를 해제하고 직접 구현합시다. 이는 시작점을 제공해 주는데, 예를 들어 커리큘럼 학습(curriculum learning)을 구현하면 모델의 오픈 루프(open-loop) 출력을 안정적으로 하는 데 도움을 줍니다.

기울기 추적을 위해 tf.GradientTape을 사용합니다. 이 방법에 대한 자세한 내용은 즉시 실행 가이드를 참조하십시오.

절차는 다음과 같이 동작합니다:

  • 먼저 RNN 상태를 초기화합니다. 우리는tf.keras.Model.reset_states 메서드를 호출하여 이를 수행합니다.

  • 다음으로 데이터셋(배치별로)를 반복하고 각각에 연관된 예측을 계산합니다.

  • tf.GradientTape를 열고 그 컨텍스트에서의 예측과 손실을 계산합니다.

  • tf.GradientTape.grads 메서드를 사용하여 모델 변수에 대한 손실의 기울기를 계산합니다.

  • 마지막으로 옵티마이저의 tf.train.Optimizer.apply_gradients 메서드를 사용하여 이전 단계로 이동합니다.

model = build_model(
  vocab_size = len(vocab),
  embedding_dim=embedding_dim,
  rnn_units=rnn_units,
  batch_size=BATCH_SIZE)
optimizer = tf.keras.optimizers.Adam()
@tf.function
def train_step(inp, target):
  with tf.GradientTape() as tape:
    predictions = model(inp)
    loss = tf.reduce_mean(
        tf.keras.losses.sparse_categorical_crossentropy(
            target, predictions, from_logits=True))
  grads = tape.gradient(loss, model.trainable_variables)
  optimizer.apply_gradients(zip(grads, model.trainable_variables))

  return loss
# 훈련 횟수
EPOCHS = 10

for epoch in range(EPOCHS):
  start = time.time()

  # 모든 에포크(Epoch)의 시작에서 은닉 상태를 초기화
  # 초기 은닉 상태는 None
  hidden = model.reset_states()

  for (batch_n, (inp, target)) in enumerate(dataset):
    loss = train_step(inp, target)

    if batch_n % 100 == 0:
      template = '에포크 {} 배치 {} 손실 {}'
      print(template.format(epoch+1, batch_n, loss))

  # 모든 5 에포크(Epoch)마다(체크포인트) 모델 저장
  if (epoch + 1) % 5 == 0:
    model.save_weights(checkpoint_prefix.format(epoch=epoch))

  print ('에포크 {} 손실 {:.4f}'.format(epoch+1, loss))
  print ('1 에포크 당 {}초 소요\n'.format(time.time() - start))

model.save_weights(checkpoint_prefix.format(epoch=epoch))
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.iter
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_1
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.beta_2
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.decay
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer.learning_rate
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-0.embeddings
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-2.kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-2.bias
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-1.cell.kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-1.cell.recurrent_kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'm' for (root).layer_with_weights-1.cell.bias
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-0.embeddings
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-2.kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-2.bias
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-1.cell.kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-1.cell.recurrent_kernel
WARNING:tensorflow:Unresolved object in checkpoint: (root).optimizer's state 'v' for (root).layer_with_weights-1.cell.bias
WARNING:tensorflow:A checkpoint was restored (e.g. tf.train.Checkpoint.restore or tf.keras.Model.load_weights) but not all checkpointed values were used. See above for specific issues. Use expect_partial() on the load status object, e.g. tf.train.Checkpoint.restore(...).expect_partial(), to silence these warnings, or use assert_consumed() to make the check explicit. See https://www.tensorflow.org/alpha/guide/checkpoints#loading_mechanics for details.
에포크 1 배치 0 손실 4.174840927124023
에포크 1 배치 100 손실 2.3459043502807617
에포크 1 손실 2.1248
1 에포크 당 8.258250713348389초 소요

에포크 2 배치 0 손실 2.328662157058716
에포크 2 배치 100 손실 1.8262289762496948
에포크 2 손실 1.7273
1 에포크 당 6.271298170089722초 소요

에포크 3 배치 0 손실 1.710461139678955
에포크 3 배치 100 손실 1.621515154838562
에포크 3 손실 1.5511
1 에포크 당 6.278482675552368초 소요

에포크 4 배치 0 손실 1.5069079399108887
에포크 4 배치 100 손실 1.4994893074035645
에포크 4 손실 1.5304
1 에포크 당 6.285874128341675초 소요

에포크 5 배치 0 손실 1.4353135824203491
에포크 5 배치 100 손실 1.434301495552063
에포크 5 손실 1.4502
1 에포크 당 6.3956217765808105초 소요

에포크 6 배치 0 손실 1.3626784086227417
에포크 6 배치 100 손실 1.361034870147705
에포크 6 손실 1.3835
1 에포크 당 6.3696370124816895초 소요

에포크 7 배치 0 손실 1.2858291864395142
에포크 7 배치 100 손실 1.2814477682113647
에포크 7 손실 1.3781
1 에포크 당 6.158828020095825초 소요

에포크 8 배치 0 손실 1.250636339187622
에포크 8 배치 100 손실 1.3254727125167847
에포크 8 손실 1.3233
1 에포크 당 6.2481279373168945초 소요

에포크 9 배치 0 손실 1.252465009689331
에포크 9 배치 100 손실 1.2833422422409058
에포크 9 손실 1.2619
1 에포크 당 6.175817966461182초 소요

에포크 10 배치 0 손실 1.1743906736373901
에포크 10 배치 100 손실 1.1858898401260376
에포크 10 손실 1.2266
1 에포크 당 6.3262152671813965초 소요