본문 바로가기

카테고리 없음

[I2S]RNN과 어텐션을 사용한 자연어 처리

문장에서 다음 글자를 예측하도록 훈련하는 문자 단위 RNN

새로운 텍스트를 생성하고 그 과정에서 매우 긴 시퀸스를 가진 텐서플로 데이터셋을 만드는 방법.

 

상태가 없는 RNN(stateless RNN)을 사용하고  다음에 상태가 있는 RNN(stateful RNN)을 구축하겠습니다.

(상태가 없는 RNN은 각 반복에서 무작위하게 택한 텍스트의 일부분으로 학습하고, 나머지 텍스트에서 어떤 정보도 사용하지 않습니다. --> 이전 반복의 정보가 들어가지 않는다는 뜻.

상태가 있는 RNN은 훈련 반복 사이에 은닉 상태를 유지하고 중지된 곳에서 이어서 상태를 반영합니다. 그래서 더 긴 패턴을 학습할 수 있습니다.). 

그 다음에는 감성 분석(영화 리뷰를 읽고 이 영화에 대한 평가자의 느낌을 추출하는 등)을 수행하는 RNN을 구추갛고 이번에는 문자가 아니라 단어의 시퀸스로 문장을 다룹니다. 그리고 신경망 기계번역(neural machine translation NMT)을 수행 할 수 있는 인코더-디코드 구조를 만들기 위해 RNN을 사용하는 방법을 알아봅니다. 도구로는 텐서플로 에드온(addon)vmfhwprxmdptj wprhdgksms seq2seq API를 사용합니다. 

 

16.4절에서 어텐션 메커니즘을 알아봅니다. 이름에서 알 수 있듯이 이는 각 타임스텝에서 모델이 집중해야할 입력 부분을 선택하도록 학습하는 신경망 구성 요소입니다. 먼저 어텐션을 사용하여  RNN기반의 인코더-디코더 구조의 성능을 높이는 방법을 알아본 뒤, RNN을 모두 제거하고 어텐션만 사용해 매우 좋은 성능을 내는 트랜스포머(Transformer)라는 구조도 살펴봅니다. 마지막으로 2018년과 2019년에 있었던 NLP분야에서 가장 중요한 발전을 살펴봅시다. 트랜스포머를 기반으로 한 GPT-2와 BERT 같은 매우 강력한 언어 모델도 포함합니다. 

(어느 정도는)셰익스피어처럼 쓸 수 있는 간단하고 재미있는 모델부터 시작해보죠.

 

16.1 Char-RNN을 사용해 셰익스피어 문체 생성하기.

2015년 The Unreasonable Effectiveness of Recurrent Neural Networks(순한 신경망의 믿을 수 없는 효율성) 이라는 제목의 블로그 글 에서 안드레아 카르파트히는 RNN을 훈련하여 문장에서 다음 글자를 예측하는 방법을 소개했습니다. 이 Char-RNN을 사용해 한 번에 한 글자씩 새로운 텍스트를 생성할 수 있습니다. 다음은 셰익스피어 작품에서 훈련한 Char-RNN 모델로 생성한 텍스트 샘플입니다. 

PANDARUS:

Alas, I think he shall be come approached and the day

When little srain would be attain'd into being never fed

And who is but a chain and subjects of his death

I should not sleep. 

판다 러스 : 아아, 그가 다가올 거라고 생각합니다.

전혀 먹지 않는 데 약간의 찌그러짐이 생길 때 그

리고 그의 죽음의 사슬과 주제에 불과한 사람

나는 자면 안된다.

 

판다루스:
아아, 나는 그가 다가와서 그 날이 올 것이라고 생각한다.
비린내 나는 것이 거의 없을 때 젖을 먹이지 않을 것이다.
그리고 누가 사슬에 불과하고 그의 죽음의 대상인가.
나는 잠을 자면 안 된다.


?

 

훌륭한 작품이라고 할 수는 없지만, 모델이 문장에서 다음 글자를 예측하도록 훈련한 것만으로도 단어, 문법, 적절한 구두점 등을 학습했다는 것은 매우 놀랍습니다. Char-RNN모델을 어떻게 만드는지 단계별로 알아봅시다. 먼저 데이터셋부터 만듭니다. 

 

16.1.1훈련 데이터셋 만들기 

케라스의 편리한 get_file()함수를 사용해 안드레이 카패시의 Char-RNN프로젝트에서 셰익스피어의 작품을 모두 다운로드 합니다. 

shakespeare_url = "https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt"
filepath = keras.utils.get_file("shakespeare.txt", shakespeare_url)
with open(filepath) as f:
    shakespeare_text = f.read()

그 다음 모든 글자를 정수로 인코딩해야 합니다. 13장에서 했던 것처럼 사용자 정의 전처리 층을 만드는 것이 한 방법입니다. 여기에서는 더 간단하게 케라스의 Tokenizer 클래스를 사용합니다. 먼저 이 클래스의 객체를 텍스트에 훈련해야 합니다. 텍스트에서 사용되는 모든 글자를 찾아 각기 다른 글자 ID에 매핑합니다. 이 ID는 1부터 시작해 고유한 글자 개수까지 만들어집니다. (나중에 보겠지만, 마스킹에 사용하기 때문에 0부터 시작하지 않습니다.) -->0은 <unk> 그래서 1부터 시작했구나....

tokenizer = keras.preprocessing.text.Tokenizer(char_level=True)
tokenizer.fit_on_texts(shakespeare_text)

char_level=True로 지정하여 단어 수준 인코딩 대신 글자 수준 인코딩을 만듭니다.-->아.. 글자수준 인코딩도 할 수 있구나. 이 클래스는 기본적으로 텍스트를 소문자로 바꿉니다.(이를 원하지 않을 경우 lower-False로 지정할 수 있습니다). 이제 문장을(또는 문장의 리스트를) 글자 ID로 인코딩하거나 반대로 디코딩할 수 있습니다. 이를 통해 텍스트에 있는 고유 글자 개수와 전체 글자 개수를 알 수 있습니다.

 

전체 텍스트를 인코딩하여 각 글자를 ID로 나타내봅시다. (1에서 39까지 대신 0에서 38까지 id를 얻기 위해 1을 뺍니다.). --> 왜 1을 빼는거지 왜 0에서 38까지로 얻는거지 0은 마스킹에 사용해야 한다며?

 

계속하기 전에 데이터셋을 훈련 세트, 검증, 세트, 테스트 세트로 나누어야함, 텍스트에 있는 글자를 섞으면 안됨, 순차 데이터셋을 어떻게 나누어야 할까? -> 뭔소리지?

 

16.1.2

훈련 세트, 검증 세트, 테스트 세트가 중복되지 않도록 만드는 것이 매우 중요합니다. 예를 들어서 텍스트의 처음 90%를 훈련 세트로 상요하고 다음 5% 검증세트 마지막 5%를 테스트 세트로 사용할 수 있습니다. 두 세트 사이에 문장이 걸치지 않고 완전히 분리될 수 잇도록 세트 사이에 간격을 두는 것도 좋은 생각입니다. 

 

시계열을 다룰 때는 보통 시간에 따라 나눕니다. 예를 들어 2000년부터 2012년까지 훈련 세트, 2013년부터 2015년까지 검증 세트, 2016년부터 2018년까지 테스트 세트로 사용합니다. 경우어 따라 훈련할 시간 간격을 더 길게 하기 위해 다른 차원을 기준으로 나눌 수도 있습니다. 가령 2000년부터 2018년까지 10,000개 회사의 재정 건전성에 대한 데이터가 있다면 회사를 기준으로 데이터를 나눌 수 있습니다. 많은 회사들이 강하게 상호 연관되어 있을 가능성이 높습니다.(예를 들어, 전체 경제 분야가 동시에 성장하거나 하락하는 등). 훈련 세트와 테스트 세트에 상호 연관된 회사가 있다면 이 테스트 세트에서 측정한 일반 오차가 낙관적으로 편향되므로 유용하지 않을 것입니다. 

 

따라서 시간에 따라 나누는 것이 안전합니다. 암묵적으로 RNN이 과거(훈련 세트)에서 학습 하는 패턴이 미래에도 등장한다고 가정합니다. 다른 말로 하면 이 시계열 데이터가(넓은 의미에서) 변하지 않는다(stationary)고 가정합니다. 많은 경우 시계열에서 이 가정이 타당합니다.(예를 들어, 화학 법칙은 시간에 따라 바뀌지 않기 때문에 화학 반응이 이에 해당합니다.)하지만 다른 많은 시계열은 그렇지 않습니다.(예를 들어 금융 시장은 변덕스럽기로 악명이 높습니다. 트레이더가 패턴을 발견 한 뒤 적용하려 하면 사라집니다.) 시게열이 진짜 충분히 안정적인지 확인하려면 시간에 따라 검증 세트에 대한 모델의 오차를 그려볼 수 있습니다. 모델이 검증 세트 마지막보다 첫 부분에서 성능이 더 좋다면 이 시계열이 충분히 안정되지 않은 것일 수 있습니다. 이럴 때는 더 짧은 시간 간격으로 모델을 훈련하는 것이 좋습니다.

 

간단하게 말해서 시계열을 훈련 세트, 검증 세트, 테스트 세트로 나누는 것은 간단한 작업이 아닙니다. 어떻게 나눌지는 주어진 문제에 따라 달라집니다.

 

다시 셰익스피어로 돌아갑시다.! 텍스트의 처음 90%를 훈련 세트로 사용합니다.(나머지는 검증 세트와 테스트로 사용합니다.) 이 세트에서 한 번에 한 글짜씩 반환하는 tf.data.Dataset 객체를 만듭니다.

train_size = dataset_size * 90 // 100
dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size])

16.1.3 순차 데이터를 윈도 여러 개로 자르기.

훈련 세트는 백만 개 이상의 글자로 이루어진 시퀸스 하나입니다. 여기에 신경망을 직접 훈련시킬 수는 없습니다. 이 RNN은 백만 개의 층이 있는 심층 신경망과 비슷하고 (매우 긴) 샘플 하나로 훈련하는 셈이 됩니다.(RNN에서는 하나의 시퀸스가 모드 처리된 후 가중치가 업데이트 됩니다.) 대신 데이터셋의 window() 메서드를 사용해 이 긴 시퀀스를 작은 많은 텍스트 윈도로 변환합니다. 이 데이터셋의 각 샘플은 전체 텍스트에서 매우 잛은 부분 문자열입니다. RNN은 이 부분 문자열 길이만큼만 역전파를 위해 펼쳐집니다. 이를 TBPTT(truncated backpropagation through time)라고 부릅니다. window() 메서드를 호출하여 짧은 텍스트 윈도를 갖는 데이터셋을 만들어보죠.

n_steps = 100
window_length = n_steps + 1 # target = input shifted 1 character ahead
dataset = dataset.repeat().window(window_length, shift=1, drop_remainder=True)

n_steps를 튜닝 할 수 있습니다. 짧은 입력 시퀸스에서 RNN을 훈련하는 것은 쉽지만 당연히 이 RNN은 n_step보다 긴 패턴을 학습 할 수 있습니니다. 따라서 너무 짧게 만들어서는 안됩니다.

 

기본적으로 window()메서드는 윈도를 중복하지 않습니다. shift=1로 지정하면 가장 큰 훈련 세트를 만들 수 있습니다. 첫번째 윈도는 0에서 100번째 글자를 포함하고 두번째 윈도는 1에서 101번째 글자를 포함하는 식입니다.(패딩 없이 배치 데이터를 만들기 위해) 모든 윈도가 동일하게 101개의 글자를 포함하도록 drop_remainde=True로 지정합니다.(그렇지 않으면 윈도 100개는 글자 100개, 글자 99개와 같은 식으로 점점 줄어 마지막 윈도는 글자 1개를 포함합니다.)

window() 메서드는 각각 하나의 데이터셋으로 표현되는 윈도를 포함하는 데이터셋을 만듭니다. 리스트의 리스트와 비슷한 중첩 데이터셋(nested dataset)입니다. 이런 구조는 데이터셋 메서드를 호출하여 각 윈도를 변환할 때 유용합니다.(예를 들어 섞거나 배치를 만듭니다.) 하지만 모델은 데이터셋이 아니라 텐서를 기대하기 때문에 훈련에 중첩 데이터셋을 바로 사용할 수 없습니다. 따라서 중첩 데이터셋을 플랫 데이터셋(데이터셋이 들어있찌 않은 데이터셋)으로 변환하는 flat_map() 메서드를 호출해야합니다. 예를 들어 {1.2.3}이 텐서 1,2,3의 시퀸스를 포함한 데이터셋이라 가정해봅시다. 중첩 데이터셋 {{1,2},{3,4,5,6}}을 평평하게 만들면 플랫 데이터셋 {1,2,3,4,5,6}이 됩니다. flat_map() 메서드는 중첩 데이터셋을 평평하게 만들기 전에 각 데이터셋에 적용할 변환 함수를 매개변수로 받을 수 있습니다. 예를 들어 lamda ds : ds.batch(2) 함수를 flat_map()에 전달하면 중첩 데이터셋 {{1,2},{3,4,5,6}}을 플랫 데이터셋 {{1,2},{3,4},{5,6}}으로 변환합니다. 이는 텐서 2개를 가진 데이터셋입니다. 이제 예제 데이터셋을 평평하게 만들 준비를 마쳤습니다.

dataset = dataset.flat_map(lambda window: window.batch(window_length))

윈도마다 batch(window_length)를 호출합니다. 이 길이는 윈도 길이와 같기 때문에 텐서 하나를 담은 데이터셋을 얻습니다. 이 데이터셋은 연속된 101 글자 길이의 윈도를 담습니다. 경사 하강법은 훈련 세트 샘플이 동일 독립 분포(4장 참조) 일때. 가장 잘 작동하기 때문에 이 윈도를 섞어야 합니다. 그 다음 윈도를 배치로 만들고(처음 100개의 글자)과 타깃(마지막 글자)를 분리하겠습니다.

batch_size = 32
dataset = dataset.shuffle(10000).batch(batch_size)
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))

그림 16-1은 지금까지 설명한 전처리 단계를 보여줍니다.(윈도 크기는 101 대신 11이고 배치 크기는 32대신 3입니다.)

 

13장에서 설명한 것처럼 일반적으로는 범주형 입력 특성은 원-핫 벡터나 임베딩으로 인코딩되어야합니다. 여기에서는 고유한 글자 수가 적기 때문에(39개) 원 핫 벡터를 사용해 글자를 인코딩합니다.

dataset = dataset.map(
    lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch))

마지막으로 프리페칭을 추가해야합니다.

dataset = dataset.prefetch(1)

16.1.4 Char-RNN 모델 만들고 훈련하기

이전 글자 100개를 기반으로 다음 글자를 예측하기 위해 유닛 128개를 가진 GRU층 2개입력(dropout)과 은닉 상태(recurrent_dropout)에 20% 드롭아웃을 사용합니다.필요하면 나중에 이 하이퍼파라미터를 수정할 수 잇습니다. 출력층은 15장에서  본 TimeDistributed 클래스를 적용한 Dense 층입니다. 텍스트에 있는 고유한 글자 수는 39개이므로 이 층은 39개의 유닛(max_id)을 가져야합니다. (타임 스텝마다)각 글자에 대한 확률을 출력할 수 있습니다. 타임 스텝에서 출력 확률의 합은 1이어야 하므로 Dense층의 출력에 소프트맥스 함수를 적용합니다. 그다음 "sparse_categorical_crossentropy" 손실과 Adam 옵티마이저를 사용해 모델의 compile() 메서드를 호출합니다. 이제 여러 에포크 동안 모델을 훈련할 준비를 마쳤습니다.

model = keras.models.Sequential([
    keras.layers.GRU(128, return_sequences=True, input_shape=[None, max_id],
                     dropout=0.2, recurrent_dropout=0.2),
    keras.layers.GRU(128, return_sequences=True,
                     dropout=0.2, recurrent_dropout=0.2),
    keras.layers.TimeDistributed(keras.layers.Dense(max_id,
                                                    activation="softmax"))
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="adam")
history = model.fit(dataset, steps_per_epoch=train_size // batch_size,
                    epochs=10)

16.1.5 Char-RNN 모델 사용하기 

이제 셰익스피어가 쓴 텍스트에서 다음 글자를 예측하는 모델이 있습니다. 이 모델에 새로운 텍스트를 주입하려면 앞에서와 같이 먼저 전처리를 해야 합니다. 이를 위해 작은 함수를 만들겠습니다.

def preprocess(texts):
    X = np.array(tokenizer.texts_to_sequences(texts)) - 1
    return tf.one_hot(X, max_id)

이제 이 모델을 사용해 어떤 텍스트의 다음 글자를 예측해봅시다.

X_new = preprocess(["How are yo"])
Y_pred = model.predict_classes(X_new)
tokenizer.sequences_to_texts(Y_pred + 1)[0][-1] # 1st sentence, last char

'u'

성공입니다! 모델이 정확히 맞혔네요. 이제 이 모델로 새로운 텍스트를 생성해봅시다.