Go together

If you want to go fast, go alone. If you want to go far, go together.

핸즈온 머신러닝 2판

16장 RNN과 어텐션을 사용한 자연어 처리

NowChan 2022. 1. 26. 19:36

우선 문장에서 다음 글자를 예측하도록 훈련하는 문자 단위 RNN(character RNN)부터 시작합니다. 새로운 텍스트를 생성하고 그 과정에서 매우 긴 시퀀스를 가진 텐서플로 데이터셋을 만드는 방법을 알아봅니다. 먼저, 상태가 없는 RNN(stateless RNN)을 사용하고 다음에 상태가 있는 RNN(stateful RNN)을 구축하겠습니다.

상태가 없는 RNN은 각 반복에서 무작위로 선택한 텍스트의 일부분으로 학습하고, 상태가 있는 RNN은 훈련 반복 사이에 은닉 상태를 유지하고 중지된 곳에서 이어서 상태를 반영합니다. 그래서 더 긴 패턴을 학습할 수 있습니다.

어텐션 메커니즘은 각 타임 스텝에서 모델이 집중해야 할 입력 부분을 선택하도록 학습하는 신경망 구성 요소입니다. 어텐션만 사용해 매우 좋은 성능을 내는 Transformer라는 구조도 살펴봅니다.

 

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


훈련 데이터셋 만들기

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

shakespeare_url = 'https://homl.info/shakespeare' # 단축URL 
filepath = keras.utils.get_file('shakespeare.txt', shakespeare_url) 
with open(filepath) as f: 			# 파일 스트림
	shakespeare_text = f.read()


그 다음 글자를 라벨 인코딩해야합니다. keras의 Tokenizer 클래스를 사용합니다. 텍스트에서 사용되는 모든 글자를 각기 다른 ID에 매핑합니다. ID는 1 ~ (고유한 글자 개수)까지 만들어집니다. char_level=True로 단어 수준 인코딩 대신 글자 수준으로 인코딩합니다. Tokenizer 클래스는 기본적으로 텍스트를 소문자로 바꿉니다.

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


이제 문장을 글자 ID로 인코딩 및 디코딩할 수 있습니다. 이를 통해 텍스트에 있는 고유 글자 개수와 전체 글자 개수를 알 수 있습니다.

tokenizer.texts_to_sequences(['First']) #[[20, 6, 9, 8, 3]] 
tokenizer.sequences_to_texts([[20, 6, 9, 8, 3]]) #['f i r s t'] 
max_id = len(tokenizer.word_index) # 고유 글자 개수
dataset_size = tokenizer.document_count # 전체 글자 개수


전체 텍스트를 인코딩해서 각 글자를 1D로 나타냅니다. 1~39까지 대신에 0~38까지 ID를 얻기 위해 1을 뺍니다.

[encoded] = np.array(tokenizer.texts_to_sequences([shakespeare_text])) -1


데이터 셋을 훈련, 검증, 테스트 세트로 나눠야 하는데 텍스트에 있는 글자를 섞으면 안됩니다. 순차 데이터셋을 어떻게 나눌까요?


순차 데이터셋을 나누는 방법

훈련, 검증, 테스트 세트를 중복되지 않게 만드는게 중요합니다. 처음 90%, 다음 5%, 다음 5%를 훈련, 검증, 테스트 세트로 사용할 수 있습니다.

시계열 데이터는 보통 시간에 따라 나눕니다. 2000~2012까지 훈련 세트, 2013~2015까지 검증 세트 등등..

물론, 훈련할 시간 간격을 더 길게 가져가기 위해 다른 기준으로 나눌 수도 있습니다. 가령 2000~2018년 까지 10,000개 회사의 재정 건정성에 대한 데이터를 회사를 기준으로 데이터를 분리할 수도 있지만, 일반적으로 테스트 세트에서 측정한 오차가 낙관적으로 편향되므로 유용하지 않습니다. 따라서 시간에 따라 나누는 것이 안전합니다.

암묵적으로 RNN은 훈련 세트에서 학습한 패턴이 미래에도 등장한다고 가정합니다. 즉, 시계열 데이터가 변하지 않는다고(stationary) 가정합니다. 화학 법칙과 같은 것은 시간에 따라 변하지 않기 때문에 타당합니다. 하지만, 대부분의 많은 시계열은 시간에 따라 안정적이지 않습니다.

더보기

정상 시계열(stationary time series)은 평균, 분산, 자기상관(autocorrelation)(즉, 어떤 간격으로 나눈 시계열 값 사이의 상관관계)이 시간에 따라 변하지 않는 것을 의미합니다. 이는 트렌드, 주기적인 패턴을 가진 시계열이 포함되지 않습니다. 따라서 매우 제한적입니다. RNN은 유연하여 트렌드와 주기적인 패턴을 학습할 수 있습니다.


시계열이 충분히 안정적인지 확인하려면 시간에 따라 검증 세트에 대한 모델의 오차를 그려볼 수 있습니다. 마지막보다 첫 부분에서 성능이 더 좋다면 시계열이 충분히 안정적이지 않다는 것일 수 있습니다. 이 때는 더 짧은 시간 간격으로 모델을 훈련하는 것이 좋습니다.

셰익스피어 텍스트는 처음 90%를 훈련 세트로 사용하고 나머지를 검증, 테스트로 사용합니다. 훈련 세트에서 한 번에 한 글자씩 반환하는 tf.data.Dataset 객체를 만듭니다.

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



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

훈련 세트는 백만 개 이상의 글자로 이루어진 하나의 시퀀스입니다. 이를 직접 훈련시키면, 이 RNN은 백만 개의 층이 있는 심층 신경망에 하나의 샘플로 훈련하는 셈입니다. RNN에서는 하나의 시퀀스가 모두 처리된 후 가중치가 업데이트되기 때문입니다.

때문에 window() 메서드를 사용해 긴 시퀀스를 작은 많은 텍스트 윈도로 변환합니다. 이 데이터셋의 각 샘플은 전체 텍스트에서 매우 짧은 부분 문자열입니다. RNN은 각 부분의 문자열 길이만큼 역전파를 위해 펼쳐집니다. 이를 TBPTT(truncated backpropagation through time)이라 부릅니다.

n_steps = 100 
window_length = n_steps + 1 # target = 1글자 앞의 input 
dataset = dataset.window(window_length, shift=1, drop_remainder=True)

Tip

더보기

RNN은 n_steps보다 긴 패턴을 학습할 수 없습니다. 따라서 너무 짧게 만들어서는 안됩니다.


window() 메서드는 shitf=1로 지정하면, 첫 번째 윈도우는 0~100번째 글자를 포함하고, 두 번째 윈도는 1~101번째 글자를 포함하는 식입니다. default는 윈도 사이즈로 여기서는 101입니다. drop_remainder=True로 지정해서 윈도우의 글자가 101개보다 적으면 버리도록 합니다. (패딩 없이 배치 데이터를 만들기 위해)

window() 메서드는 윈도들을 담은 데이터셋을 반환합니다. 리스트의 리스트와 비슷한 중첩 데이터셋(nested dataset)입니다. 중첩 데이터셋 구조는 각 윈도 별로 dataset 메서드(섞거나 배치를 만드는 것)를 호출할 때 유용합니다.

RNN 등 모델은 데이터셋이 아닌 텐서를 기대하기 때문에, flat_map()을 호출해 중첩 데이터셋플랫 데이터셋(flat dataset)으로 바꿔줍니다. 예를 들어 {{1, 2}, {3, 4, 5, 6}}을 평평하게 만들면 플랫 데이터셋은 {1, 2, 3, 4, 5, 6}이 됩니다. flat_map() 메서드는 매개변수로 적용할 변환 함수를 전달할 수 있습니다. lambda ds: ds.batch(2) 함수를 전달하면 중첩 데이터셋{{1, 2}, {3, 4, 5, 6}}을 플렛 데이터셋 {[1, 2], [3, 4], [5, 6]}으로 변환합니다. 이는 텐서 3개를 가진 데이터셋입니다.

dataset = dataset.flat_map(lambda window: window.batch(window_length)) 
# ex 
''' 
tf.Tensor( [19 5 8 7 2 0 18 5 2 5 35 1 9 23 10 21 1 19 3 8 1 
0 16 1 0 22 8 3 18 1 1 12 0 4 9 15 0 19 13 8 2 6 1 8 17 0 6 1
4 8 0 14 1 0 7 22 1 4 24 26 10 10 4 11 11 23 10 7 22 1 4 24 17
0 7 22 1 4 24 26 10 10 19 5 8 7 2 0 18 5 2 5 35 1 9 23 10 15 3
13 0], shape=(101,), dtype=int64) 

tf.Tensor( [ 5 8 7 2 0 18 5 2 5 35 1 9 23 10 21 1 19 3 8 1 0 16
1 0 22 8 3 18 1 1 12 0 4 9 15 0 19 13 8 2 6 1 8 17 0 6 1 4 8 0 14 
1 0 7 22 1 4 24 26 10 10 4 11 11 23 10 7 22 1 4 24 17 0 7 22 1 4 
24 26 10 10 19 5 8 7 2 0 18 5 2 5 35 1 9 23 10 15 3 13 0 4], shape=(101,),
dtype=int64)

tf.Tensor( [ 8 7 2 0 18 5 2 5 35 1 9 23 10 21 1 19 3 8 1 0 16 1 0 22 8 3 18
1 1 12 0 4 9 15 0 19 13 8 2 6 1 8 17 0 6 1 4 8 0 14 1 0 7 22 1 4 24 26 10 10 
4 11 11 23 10 7 22 1 4 24 17 0 7 22 1 4 24 26 10 10 19 5 8 7 2 0 18 5 2 5 35
1 9 23 10 15 3 13 0 4 8], shape=(101,), dtype=int64) 
'''

배치 크기가 윈도 길이와 같아서 각 텐서 하나 하나를 통째로 담은 데이터셋이 반환됩니다.

경사 하강법은 훈련 세트 샘플이 동일 독립 분포일 때 가장 잘 작동해서 데이터를 섞어야합니다. 섞은 다음 윈도를 배치로 만들고 입력(처음 100개 글자)와 타깃(마지막 100개 글자)를 분리하겠습니다.

batch_size=32 
dataset = dataset.shuffle(10000).batch(batch_size) 
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:])) 
# 첫 번째 100개 글자와 한 글자 다음 100개 글자를 튜플에 담습니다.?

 


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

dataset = dataset.map( lambda X_batch, Y_batch: (tf.one_hot(X_batch, depth=max_id), Y_batch) )
# Y_batch를 인코딩하지 않는 이유는 X_batch의 원-핫 벡터를 학습해 
# 예측한 결과를 앞서 인코딩한 Y_Batch와 비교할 것이라서


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

dataset = dataset.prefetch(1)


자! 이제 데이터셋 준비가 완료되었고, 모델을 만들어보겠습니다.


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

model = keras.models.Sequential([
        keras.layers.GRU(128, return_sequences=True, input_shape=[None, max_id], # one-hot encoding
                         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, epochs=20)
'''
Epoch 1/20
     63/Unknown - 32s 420ms/step - loss: 3.1154 ...

훈련할 때 많은 시간이 걸립니다.
'''

 

텍스트에 있는 고유한 글자수가 39개이므로 이 층은 39개의 유닛(max_id)을 가져야 합니다. 타임 스텝마다 각 글자에 대한 확률을 출력할 수 있습니다. 타임 스텝에서 출력 확률의 합은 1이어야 하므로 Dense 층에서 softmax 함수를 적용합니다.

 

 

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 = np.argmax(model(X_new), axis=-1)
tokenizer.sequences_to_texts(Y_pred+1)[0][-1] # 첫 번째 문장, 마지막 글자
#'u'

모델을 완전히 학습 시키면 잘 예측될 것입니다.

 

 

가짜 셰익스피어 텍스트를 생성하기

 

Char-RNN 모델로 새로운 텍스트를 생성하려면 초기 텍스트를 주입하고 모델이 가장 가능성이 있는 다음 글자를 예측합니다. 이 글자를 텍스트 끝에 추가하고 늘어난 텍스트를 모델에 전달하여 다음 글자를 예측하는 식입니다. 이렇게 하면 실제로는 같은 단어가 반복되는 경우가 많습니다. tf.random.categorical() 함수를 사용해 모델이 추정한 확률을 기반으로 다음 글자를 무작위로 선택할 수 있습니다.

 

categorical() 함수에 클래스의 로그 확률(로짓)을 전달하면 랜덤하게 클래스 인덱스를 샘플링합니다. 온도(temperature)라고 불리는 숫자로 로짓을 나누는데, 온도를 크게 설정하면 모든 글자를 동일한 확률로 샘플링합니다.

 

def next_char(text, temperature=1):
  X_new = preprocess([text])
  y_proba = model(X_new)[0, -1:, :] # shape = (1, 39)
  # model(X_new)[0, -1, :].shape = (39,)
  # model(X_new)[0, :, :].shape = (5, 39)
  rescaled_logits = tf.math.log(y_proba) / temperature
  # tf.random.categorical(x, s)에서 x에 입력된 확률에 따라 표본 s개의 index(클래스)를 추출합니다.
  char_id = tf.random.categorical(rescaled_logits, num_samples=1) +1
  # char_id.numpy() tensor → ndarray, [0]은 list → char
  return tokenizer.sequences_to_texts(char_id.numpy())[0]

 

next_char() 함수를 반복하여 호출하고 다음 글자를 텍스트에 추가하는 complete_text 함수를 만듭니다.

 

def complete_text(text, n_chars=50, temperature=1):
  for _ in range(n_chars):
    text += next_char(text, temperature)
  return text

 

온도를 다르게 하며 테스트합니다. 셰익스피어 모델은 1에 가까운 온도에서 잘 작동합니다. 더 좋은 텍스트를 생성하려면 GRU 층과 층의 뉴런 수를 늘리고 더 오래 훈련, 규제(GRU 층을 recureent_dropout=0.3으로 지정할 수있습니다.) 

 

print(complete_text('t', temperature=0.2))
'''
the belly the great and who shall be the belly the
'''
print(complete_text('w', temperature=1))
'''
thing? or why you gremio. who make which the first
'''
print(complete_text('w', temperature=1))
'''
th no cce:
yeolg-hormer firi. a play asks.
fol rusb
'''

 

현재 글자 100개인 n_steps보다 긴 패턴을 학습할 수 없는데, 윈도를 크게할 수 있지만 훈련이 어려워집니다.  LSTM, GRU 셀은 매우 긴 시퀀스를 다룰 수 없습니다. 아니면 상태가 있는 RNN(stateful RNN)을 사용합니다.

 

 

상태가 있는 RNN

 

지금까지는 상태가 없는 RNN만 사용했습니다. 타임 스텝마다 이 상태를 업데이트하고 마지막 타임 스텝 후에는 더 필요가 없기 때문에 버립니다. 즉, 배치 반복마다 모델의 은닉 상태를 0으로 초기화합니다.

 

상태가 있는 RNN(statful RNN)은 한 배치를 처리한 후 마지막 상태를 다음 배치의 초기 상태로 사용합니다. 이렇게 하면 역전파는 짧은 시퀀스에서 일어나지만 모델이 장기간 패턴을 학습할 수 있습니다. 한 에폭이 끝나면 상태를 초기화 합니다. 에폭은 같은 데이터를 반복해 훈련하는 것이기 때문입니다.

 

상태가 있는 RNN은 각 입력 시퀀스가 이전 배치의 시퀀스가 끝난 지점에서 시작해야 합니다. 따라서 순차적이고 겹치지 않는 입력 시퀀스를 만들어야 합니다. Dataset을 만들 때 window() 메서드에 shift=n_steps를 사용하고, shuffle() 메서드를 호출하면 안됩니다.

 

배치를 만들 때 단순히 batch(32)로 호출하면, 문제가 발생합니다. 가령, 첫 번재 배치에서는 윈도우 1~32까지 포함하고, 두 번째 배치는 윈도 33~64까지 포함합니다. 윈도 1가 입력된 곳에 윈도 2가 아닌, 33이 들어가는 것입니다. 이 문제를 해결하는 방법은 하나의 윈도를 갖는 배치를 만드는 것입니다.

 

dataset = tf.data.Dataset.from_tensor_slices(encoded[:train_size])
dataset = dataset.window(window_length, shift=n_steps, drop_remainder=True)
dataset = dataset.flat_map(lambda window: window.batch(window_length))
dataset = dataset.batch(1)
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))
dataset = dataset.map(
    lambda X_batch, Y_batch : (tf.one_hot(X_batch, depth=max_id), Y_batch)
)
dataset = dataset.prefetch(1)

 

batch_size = 32
encoded_parts = np.array_split(encoded[:train_size], batch_size)
datasets = []
for encoded_part in encoded_parts:
  dataset = tf.data.Dataset.from_tensor_slices(encoded_part)
  dataset = dataset.window(window_length, shift=n_steps, drop_remainder=True)
  dataset = dataset.flat_map(lambda window: window.batch(window_length))
  datasets.append(dataset)
dataset = tf.data.Dataset.zip(tuple(datasets)).map(lambda *windows: tf.stack(windows))
dataset = dataset.map(lambda windows: (windows[:, :-1], windows[:, 1:]))
dataset = dataset.map(
    lambda X_batch, Y_batch : (tf.one_hot(X_batch, depth=max_id), Y_batch)
)
dataset = dataset.prefetch(1)

 

위 코드를 그림으로 설명

 

아래 코드에서 한 배치에서 n 번째 입력 시퀀스의 끝과 다음 배치의 n번째 입력 시퀀스의 시작이 같습니다. 

 

tf.train.Dataset.zip(datasets).map(lambda *windows: tf.stack(windows))

 

이제 상태가 있는 RNN을 만들어보겠습니다. 각 순환 층을 만들 때 stateful=True로 지정해야 합니다.

model = keras.models.Sequential([
        keras.layers.GRU(128, return_sequences=True, stateful=True,
                         dropout=0.2, recurrent_dropout=0.2,
                         batch_input_shape=[batch_size, None, max_id]),
        keras.layers.GRU(128, return_sequences=True, stateful=True,
                         dropout=0.2, recurrent_dropout=0.2),
        keras.layers.TimeDistributed(keras.layers.Dense(max_id,
                                                        activation='softmax'))
])

 

에포크 끝마다 텍스트를 다시 시작하기 전에 상태를 재설정해야 합니다. 콜백 함수를 사용하여 처리할 수 있습니다.

class ResetStatesCallback(keras.callbacks.Callback):
  def on_epoch_begin(self, epoch, logs):
    self.model.reset_states() # ?

 

모델을 컴파일하고 훈련합니다. 배치마다 샘플이 하나여서 개별 에포크는 이전보다 훨씬 빨리 끝나므로 여러 에포크 동안 훈련합니다.

model.compile(loss='sparse_categorical_crossentropy', optimizer='adam')
model.fit(dataset, epochs=50,
          callbacks=[ResetStatesCallback()])

 

Tip

더보기

이 모델을 훈련한 후에는 사용한 것과 동일한 크기의 배치로만 예측을 만들 수 있습니다. 이런 제약을 없애려면 동일한 구조의 상태가 없는 모델을 만들고 상태가 있는 모델의 가중치를 복사합니다.

 

지금까지 글자 수준의 모델을 만들었습니다. 이제 단어 수준 모델을 살펴보고 자연어 처리인 감성 분석(sentimental analysis)을 다루어봅시다. 이 과정에서 masking을 사용해 길이가 다른 시퀀스를 다루는 방법도 알아봅시다.

 

 

감성 분석

 

(X_train, y_train), (X_test, y_test) = keras.datasets.imdb.load_data()
X_train[0][:10] # [1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65]

 

각 정수는 하나의 단어를 나타냅니다. 구두점(마침표, 등등)을 모두 제거하고, 단어는 소문자로 변환한 다음 공백을 기준으로 빈도에 따라 인덱스를 붙였습니다. (낮은 정수가 자주 등장하는 단어)

 

정수 0, 1, 2는 조금 특별한데, 각각 패딩 토큰, SOS(start-of-sequence) 토큰, 알 수 없는 단어를 의미합니다.

 

word_index = keras.datasets.imdb.get_word_index()
id_to_word = {id_ + 3: word for word, id_ in word_index.items()}
for id_, token in enumerate(("<pad>", "<sos>", "<unk>")):
  id_to_word[id_] = token

' '.join([id_to_word[id_] for id_ in X_train[0][:10]])
# <sos> this film was just brilliant casting location scenery story

 

실전 프로젝트에서는 직접 텍스트를 전처리해야합니다. 앞서 보았던 Tokenizer 클래스를 사용할 수 있습니다. 이번에는 char_level=False로 설정합니다. 구두점, 줄바꿈 탭 등은 단어를 인코딩할 때 제외됩니다.(filters 매개 변수로 변경 가능)

 

각 나라마다 언어 규칙이 다른데, 공백으로 단어를 구분하지 않는 경우 subword(부분 단어) 수준으로 텍스트를 토큰화하거나 복원하는 비지도 학습 방법을 사용할 수 있습니다. 이 방법은 공백도 하나로 문자로 취급하기 때문에 모든 언어에서 사용 가능합니다. 이 방식은 모델이 이전에 본 적이 없는 단어를 만나더라도 의미를 추측할 수 있습니다. 예를 들어, smartest라는 단어를 본 적이 없지만 smart란 단어와 접미다 '-est'가 the most란 의미를 학습할 수 있습니다.

 

TF.Text 라이브러리에는 wordpiece를 포함한 다양한 토큰화 전략이 구현되어 있습니다.

 

모델을 모바일 기기, 웹 브라우저에 배포하고 싶다면 전처리 과정을 모델 자체에 포함시켜 매번 다른 전처리 함수를 작성하지 않을 수 있습니다.

 

먼제 IMDb 리뷰를 텍스트(바이트 스트링)로 적재합니다.

import tensorflow_datasets as tfds

datasets, info = tfds.load('imdb_reviews', as_supervised=True, with_info=True)
train_size = info.splits['train'].num_examples

 

전처리 함수를 만듭니다.

 

def preprocess(X_batch, y_batch):
  X_batch = tf.strings.substr(X_batch, 0, 300)
  X_batch = tf.strings.regex_replace(X_batch, b"<br\\s*/?>", b" ")
  X_batch = tf.strings.regex_replace(X_batch, b"[^a-zA-Z']", b" ")
  X_batch = tf.strings.split(X_batch)
  return X_batch.to_tensor(default_value=b"<pad>"), y_batch

 

리뷰 텍스트 길이를 처음 300 글자만 남깁니다. 보통 리뷰가 긍정, 부정인지는 처음 한 두 문장에서 판단할 수 있어서 성능에 크게 영향을 미치지 않습니다. 그 다음 정규식(regular expression)을 사용해 <br /> 태그를 공백으로 바꿉니다. 문자와 작은 따옴표가 아닌 모든 문자를 공백으로 바꿉니다. "Well, I can't<br />" → "Well I can't"가 될 것입니다. 

 

위에서 만든 전처리 함수를 적용하면 리뷰를 공백으로 나눕니다. 이때 래그드 텐서(ragged tensor)가 반환됩니다. 그리고 레그드 텐서를 밀집 텐서로 바꾸고 동일한 길이가 되도록 패딩 토큰 '<pad>'로 모든 리뷰를 패딩합니다.

 

Tip

더보기

to_tensor() 메서드에서 default_value를 지정하지 않으면 빈 바이트 문자열로 패딩됩니다.

 

그 다음 어휘 사전을 구축해야 하는데, 전체 훈련 세트를 한 번 순회하면서 preprocess() 함수를 적용하고 Counter로 단어의 등장 횟수를 셉니다.

 

from collections import Counter
vocabulary = Counter()
for X_batch, y_batch in datasets['train'].batch(32).map(preprocess):
  for review in X_batch:
    vocabulary.update(list(review.numpy()))

 

가장 많이 등장하는 단어 3개를 확인해보겠습니다.

 

vocabulary.most_common()[:3]
# [(b'<pad>', 214309), (b'the', 61137), (b'a', 38564)]

 

좋은 성능을 내기 위해 모든 단어를 알아야할 필요는 없으므로, 자주 등장하는 단어 10,000개만 남기고 삭제합니다.

 

vocab_size= 10000
truncated_vocabulary = [
    word for word, count in vocabulary.most_common()[:vocab_size]
]

 

이제 각 단어를 1D(즉, 어휘 사전의 인덱스)로 바꾸는 전처리 단계를 추가합니다. 1000개의 oov(out-of-vocabulary) 버킷을 사용하는 룩업 테이블(lookup table)을 만듭니다.

 

words = tf.constant(truncated_vocabulary)
word_ids = tf.range(len(truncated_vocabulary), dtype=tf.int64)
vocab_init = tf.lookup.KeyValueTensorInitializer(words, word_ids)
num_oov_buckets = 1000
table = tf.lookup.StaticVocabularyTable(vocab_init, num_oov_buckets)

 

이제 테이블에서 단어 몇 개에 대한 ID를 확인해보겠습니다.

 

table.lookup(tf.constant([b"This movie was faaaaaantastic".split()]))
# <tf.Tensor: shape=(1, 4), dtype=int64, numpy=array([[   22,    12,    11, 10053]])>

 

Tip

더보기

(13장에서 소개한) TF 변환에서 이런 어휘 사전을 편리하게 다룰 수 있는 함수들을 제공합니다. 예를 들어 tft.compute_and_apply_vocabulary() 함수는 데이터셋에서 고유한 모든 단어를 찾아 어휘 사전을 구축합니다. 또한 이 어휘 사전을 사용해 각 단어를 인코딩 하기 위한 TF 연산을 만듭니다.

 

이제 최종 훈련 세트를 만들 준비가 되었습니다. 리뷰를 배치로 묶고 preprocess() 함수를 사용해 단어(짧은 시퀀스)로 바꾸겠습니다. 그 다음 앞서 만든 테이블을 사용하는 encode_words() 함수로 단어를 인코딩합니다. 마지막으로 다음 배치를 프리페치합니다.

 

def encode_words(X_batch, y_batch):
  return table.lookup(X_batch), y_batch

train_set = datasets['train'].batch(32).map(preprocess)
train_set = train_set.map(encode_words).prefetch(1)

 

embed_size=128
model = keras.models.Sequential([
        keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size,
                               input_shape=[None]),
        keras.layers.GRU(128, return_sequences=True),
        keras.layers.GRU(128),
        keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam',
              metrics=['accuracy'])
history = model.fit(train_set, epochs=5)

첫 번째 층은 단어 ID를 (13장에서 소개한) 임베딩으로 변환하는 Embedding 층입니다. 임베딩 행렬은 단어 ID 당 (vocab_size + num_oov_buckets) 하나의 행과 임베딩 차원당 (이 예에서는 128을 사용하지만 이는 튜닝할 수 있는 파라미터 입니다.) 하나의 열을 가집니다. 모델의 입력은 [배치 크기, 타임 스텝 수] 크기를 가진 2D 텐서이지만, Embedding 층의 출력은 [배치 크기, 타임 스텝 수, 임베딩 크기] 크기를 가진 3D 텐서가 됩니다.

 

모델의 나머지 부분은 GRU 층 두개를사용하고 두 번째 층은 마지막 타임 스텝의 출력만 반환합니다. 출력층은 sigmoid를 사용하는 하나의 뉴런입니다. 리뷰가 영화에 대한 긍/부정 감정을 표현하는지에 대한 추정 확률입니다.

 

 

마스킹

 

원래 데이터 그대로 모델이 패딩 토큰을 무시하도록 학습되어야 합니다. Embedding 층을 만들 때 mask_zero=True로 지정하면 이어지는 모든 층에서 (ID가 0인) 패딩 토큰을 무시합니다. 이게 전부입니다! 

 

※ 데이터셋에서 가장 자주 등장하는 단어일 때 ID가 0이 됩니다. 실제로 가장 자주 등장하지 않더라도 패딩 토큰을 항상 0으로 인코딩하는 것은 좋은 방법입니다.

 

구체적으로 Embedding 층이 K.not_equal(inputs, 0)과 같은 mask tensor를 만듭니다.(K=keras.backend) 이 텐서는 입력과 크기가 같은 boolean tensor입니다. ID가 0인 위치는 False이고 나머지는 True입니다. 이 mask tensor는 타임 스텝 차원이 유지되는 모든 층에 자동으로 전파됩니다. 여기서는 두 개의 GRU 층이 자동으로 이 mask tensor를 받습니다. 두 번째 GRU 층이 마지막 타임 스텝만 출력해서 Dense 층에는 마스크 텐서가 전달되지 않습니다. 일반적으로 마스킹된 타임 스텝(False로 마스킹된 타임 스텝)을 무시합니다. 순환층은 마스킹된 타임 스텝을 만나면 이전 타임 스텝의 출력을 단순히 복사합니다.

 

만약 마스크가 출력에도 전파된다면(only 시퀀스를 출력하는 모델) 손실에도 적용될 수 있지만, 이 타임 스텝의 손실이 0이라 영향을 미치지 못할 것입니다.

 

CAUTION_

더보기

LSTM과 GRU 층은 엔비디아의 cuDNN 라이브러리에 기반한 최적화된 GPU 구현을 가지고 있습니다. 하지만, 이 구현은 마스킹을 지원하지 않습니다. 만약 모델이 마스킹을 사용하면 이 층들은 (훨씬 느린) 기본 구현을 사용합니다. 또한 최적화된 구현을 사용하려면 activation, recurrent_activation, recurrent_dropout, unroll, use_bias, reset_after 매개변수를 기본값으로 지정해야 합니다.

 

마스크를 받는 모든 층은 마스킹을 지원해야 합니다. 모든 순환 층은 물론 TimeDistributed 층과 몇 개의 다른 층도 포함됩니다. 마스킹을 지원하는 모든 층은 supports_masking = True입니다.

 

마스킹을 지원하는 사용자 정의 층을 구현하려면 call() 메서드에 mask 매개변수를 추가해야 합니다. 또한 생성자에 self.supports_masking = True로 지정해야 합니다.

 

사용자 정의 층이 Embedding 층으로 시작하지 않는다면 keras.layers.Masking 층을 사용할 수 있습니다. 이 층은 마스크를 K.any(K.not_equal(inputs, 0), axis=-1)로 세팅합니다. 마지막 차원이 0으로 채워진 타임 스텝은 모든 층에서 마스크 처리됩니다. (여기에서도 타임 스텝 차원이 유지되는 동안입니다.)

 

마스킹 층과 마스킹 자동 전파는 Sequential 모델에 가장 잘 맞습니다. Conv1D 층과 순환 층을 섞는 것과 같이 복잡한 모델에서는 항상 작동하지 않습니다. 이런 경우 함수형 API, 서브클래싱 API를 사용해 직접 마스크를 계산해 다음 층에 전달해야합니다. 아래 코드는 이전 모델과 동일하지만, 함수형 API를 사용하여 직접 마스킹을 처리합니다.

 

K = keras.backend
inputs = keras.layers.Input(shape=[None])
mask = keras.layers.Lambda(lambda inputs: K.not_equal(inputs, 0))(inputs)
z = keras.layers.Embedding(vocab_size + num_oov_buckets, embed_size)(inputs)
z = keras.layers.GRU(128, return_sequences=True)(z, mask=mask)
z = keras.layers.GRU(128)(z, mask=mask)
outputs = keras.layers.Dense(1, activation='sigmoid')(z)
model = keras.Model(inputs=[inputs], outputs=[outputs])

 

몇 번의 에포크를 훈련하고 나면 리뷰가 긍정/부정인지 꽤 잘 판단합니다. TensorBoard() 콜백을 사용하면 텐서보드에서 학습된 임베딩을 시각화할 수 있습니다. 비슷한 단어끼리 임베딩 되어있을 것입니다.

 

데이터가 많으면 더 휼륭한 임베딩이 만들어집니다. 대량의 텍스트 corpus (위키백과 문서 같은)에서 훈련된 단어 임베딩을 재사용할 수 있습니다. 영화 리뷰가 아니더라도 일반적인 amazing 과 같은 단어는 다른 것에서 이야기할 때도 비슷한 의미를 가집니다. 

 

또한 awesome, amazing과 같은 단어는 비슷한 의미를 가지니 임베딩이 다른 작업(다음 단어 예측하기와 같은)에서도 임베딩 공간에 군집을 이룰 가능성이 높습니다. 따라서 사전 훈련된 임베딩을 재사용하 많은 파라미터를 사용해 단어 임베딩을 학습하는 수고를 덜 수 있는지 검토해야합니다.

 

 

사전훈련된 임베딩 재사용하기

 

텐서플로 허브(Hub)는 모델 컴포넌트를 모델에 추가하기 쉽게 만들어 줍니다. 이런 모델 컴포넌트를 모듈(module)이라 부릅니다. 브라우저로 TF 허브 저장소에서 원하는 모듈을 찾아 예제 코드를 프로젝트로 복사할 수 있습니다. 모듈과 사전훈련된 가중치를 자동으로 다운로드하여 모델에 포함시킵니다.

 

예를 들어 nnlm-en-dim50 문장 임베딩 모듈 버전 1을 감성 분석 모델에 사용해보겠습니다.

 

import tensorflow_hub as hub

model = keras.Sequential([
        hub.KerasLayer('https://tfhub.dev/google/tf2-preview/nnlm-en-dim50/1',
                       dtype=tf.string, input_shape=[], output_shape=[50]),
        keras.layers.Dense(128, activation='relu'),
        keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam',
              metrics=['accuracy'])

 

hub.KerasLayer 층이 주어진 URL에서 모듈을 다운로드합니다. 이 모듈의 이름은 문장 인코더(sentence encoder)입니다. 문자열을 입력으로 받아 하나의 벡터로 인코딩합니다.(여기서는 50차원의 한 벡터) 대규모 코퍼스에서 사전 훈련된 임베딩 행렬을 사용해 각 단어를 임베딩합니다. 그 다음 모든 단어 임베딩의 평균을 계산합니다. 이 결과가 문장 임베딩입니다. 문장 임베딩은 단어 임베딩의 평균과 문장에 있는 단어 수의 제곱근을 곱한 것입니다. 단어 수의 제곱근은 n이 커짐에 따라 임베딩의 평균이 작아지는 것을 보상해줍니다.

 

그 다음 2개의 Dense 층을 추가해 감성 분석 모델을 만듭니다. 기본적으로 hub.KerasLayer 층은 훈련되지 않지만, trainable=True로 설정하여 작업에 맞게 미세 조정할 수 있습니다.

 

CAUTION_

더보기

TF 허브 모듈이 모두 텐서플로 2를 지원하지 않습니다. 모듈을 선택할 때 주의하세요.

 

그 다음 IMDb 리뷰 데이터셋을 다운로드합니다. (배치와 프리페치를 제외하고) 따로 전처리할 필요가 없습니다. 바로 모델을 훈련할 수 있습니다.

 

datasets, info = tfds.load('imdb_reviews', as_supervised=True, with_info=True)
train_size = info.splits['train'].num_examples
batch_size = 32
train_set = datasets['train'].batch(batch_size).prefetch(1)
history = model.fit(train_set, epochs=5)

 

TF 허브 모듈 URL의 마지막 부분은 필요한 버전을 지정합니다. 따라서 새로운 버전의 모듈이 릴리스되더라도 모델에 영향을 주지 않습니다. 이 URL을 웹 브라우저에 입력하면 모듈에 관한 문서를 볼 수 있습니다.

 

TF 허브를 다운로듷나 파일을 로컬 시스템의 임시 디렉터리에 캐싱합니다. 시스템을 정리할 때마다 다시 다운로드를 피하려면 고정 디렉터리에 다운로드할 수 있습니다. TFHUB_CACHE_DIR 환경 변수에 원하는 디렉터리를 지정합니다. 예를 들면,  (os.environ['TFHUB_CACHE_DIR']='./my_tfhub_cache')

 

 

신경망 기계 번역을 위한 인코더-디코더 네트워크

 

영어 문장을 프랑스어로 번역하는 간단한 신경망 기계 번역 모델을 살펴봅시다.

 

그림 16-3

 

 

디코더에서는 이전 스텝의 출력된 단어를 다음 스텝의 입력으로 사용합니다. 맨 처음 단어는 SOS(start-of-sequence) 토큰으로 시작합니다. 디코더는 문장 끝에 EOS(end-of-sequence) 토큰이 있을 것으로 기대합니다. [?, 훈련시 타깃을 모델에 주입하는 이유는 프랑스어 임베딩과 영어 임베딩의 위치를 mapping하는 걸 학습시키기 위해서?]

 

영어 문장은 인코더에 주입되기 전 거꾸로 뒤집힙니다. 'I drink milk'는 뒤집혀 'milk drink I'가 됩니다. 이 단어가 디코더가 번역할 첫 번째 단어이기 때문입니다. 그 다음 임베딩 층이 단어 임베딩을 반환하고 이 단어 임베딩이 인코더와 디코더로 주입됩니다.

 

각 단계마다 디코더는 출력 어휘 사전(프랑스어)에 있는 단어에 대한 점수를 출력합니다. 그 다음 소프트맥스 층이 이 점수를 확률로 바꿉니다. 예를 들어 첫 번째 스텝에서 단어 Je는 20%의 확률을 갖고 Tu는 1%의 확률을 갖는 식입니다. 가장 높은 확률의 단어가 출력됩니다. 이는 일반적인 분류 작업과 비슷합니다. 따라서 Char-RNN 모델에서 했던 것처럼 'sparse_categorical_crossentropy' 손실 함수를 사용해 훈련할 수 있습니다.

 

 

훈련이 끝난 후 추론 시에는 디코더에 주입할 타깃 문장이 없습니다. 대신 위 그림처럼 그냥 이전 스텝에서 디코더가 출력한 단어를 주입합니다. (여기서도 임베딩 룩업이 필요하지만 그림에 나타내지 않았습니다.)

 

모델을 구현할 때 추가적으로 처리해야할 것들이 조금 더 있습니다.

  • 지금까지는 인코더와 디코더의 입력 시퀀스 길이가 같다고 가정했지만, 실제 입력 문장의 길이는 다를 수 있습니다. 전체 문장을 번역해야하기 때문에 감성 분석처럼 마스킹을 이용해 자를 수도 없습니다. 대신 비슷한 길이의 문장을 담은 버킷으로 그룹핑하는 것입니다. 예를 들어, 한 버킷은 1~6개의 단어로 이루어진 문장을 담고, 또 다른 버킷은 7~12개의 달어로 이루어진 문장을 담는 식입니다. 그 후 버킷에 담긴 문장이 모두 동일한 길이가 되도록 패딩을 추가합니다. 예를 들어, 'I drink milk'는 '<pad> <pad> <pad> milk drink I.'가 됩니다. (tf.data.experimental.bucket_by_sequence_length() 함수를 확인해보세요)
  • EOS 토큰 이후 출력은 모두 무시합니다. 이 토큰들은 손실에 영향을 미치지 않습니다. (마스킹 처리되어야 합니다.) 예를 들어 모델이 'Je bois du lait <eos> oui'을 출력하면 마지막 단어에 대한 손실은 무시합니다.
  • 출력 어휘 사전이 프랑스어 단어 50,000개를 가진다면 디코더는 50,000차원의 벡터를 출력할 것입니다. 이런 큰 백터를 소프트맥스 함수를 계산하는 데 연산 비용이 높습니다. 한 가지 방법은 샘플링 소프트맥스(sampled softmax)를 사용하는 것입니다. 이는 타깃 단어에 대한 로짓과 타깃이 아닌 단어 중 무작위로 샘플링한 단어의 로짓만 고려하는 것입니다. 텐서플로에서는 훈련 시 tf.nn.sampled_softmax_loss() 함수를 사용하고 추론 시에는 일반 소프트맥스 함수를 사용할 수 있습니다. 샘플링 소프트맥스는 타깃을 알고 있어야 하므로 추론 시에는 사용할 수 없습니다.

 

텐서플로 Addon 프로젝트는 seq2seq 도구를 여러 개 가지고 있어 제품 수준의 인코더-디코더를 손쉽게 만들 수 있습니다. 다음 코드는 그림 16-3에 나타난 것과 비슷한 기본적인 인코더-디코더 모델입니다.

 

import tensorflow_addons as tfa

encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
sequence_lengths = keras.layers.Input(shape=[], dtype=np.int32)

embeddings = keras.layers.Embedding(vocab_size, embed_size)
encoder_embeddings = embeddings(encoder_inputs)
decoder_embeddings = embeddings(decoder_inputs)

encoder = keras.layers.LSTM(512, return_state=True)
encoder_outputs, state_h, state_c = encoder(encoder_embeddings)
encoder_state = [state_h, state_c]

sampler = tfa.seq2seq.sampler.TrainingSampler()

decoder_cell = keras.layers.LSTMCell(512)
output_layer = keras.layers.Dense(vocab_size)
decoder = tfa.seq2seq.basic_decoder.BasicDecoder(decoder_cell, sampler,
                                                 output_layer = output_layer)
final_outputs, final_state, final_sequence_lengths = decoder(
    decoder_embeddings, initial_state = encoder_state,
    sequence_length = sequence_lengths
)
Y_proba = tf.nn.softmax(final_outputs.rnn_output)

model = keras.Model(inputs=[encoder_inputs, decoder_inputs, sequence_lengths],
                    outputs=[Y_proba])

 

코드의 대부분은 이해가 잘 되지만, 몇 가지 언급할 점이 있습니다.

  1. LSTM 층을 만들 때 return_state=True로 해서 장·단기 은닉 상태가 반환됩니다.
  2. TrainingSampler는 각 스텝에서 디코더에게 이전 스텝의 출력이 무엇인지 알려줍니다. 훈련 시에는 이전 타깃 토큰 임베딩을 알려주고, 추론 시에는 실제 출력되는 토큰의 임베딩을 알려줍니다.

실전에서는 이전 타임 스텝의 타깃 임베딩을 사용해 훈련을 시작하다가 점차 이전 스텝에서 출력된 실제 토큰의 임베딩으로 바꾸는 것이 좋습니다. ScheduledEmbeddingTrainingSampler는 타깃실제 출력을 무작위로 선택해 훈련하는 동안 점진적으로 확률을 바꿀 수 있습니다.

 

 

양방향 RNN

 

일반적인 순환 층은 과거와 현재의 입력만 보고 출력을 생성합니다. causal합니다. 즉, 미래를 볼 수 없습니다. 이런 종류의 RNN은 시계열을 예측할 때 적합하지만, NLP 작업에는 맞지 않습니다.

 

NLP는 주어진 단어를 인코딩하기 전에 다음 단어를 미리 보는 것이 좋습니다. 'the Queen of the United Kingdom', 'the queen of hearts', 'the queen bee'같이 통체로 이해해야하는 구 단어들이 있을 수 있습니다. 이런 구 단어를 알아야 queen도 올바르게 인코딩할 수 있습니다. 이를 위해 동일한 입력에 대해 2개의 순환층을 실행합니다. 하나는 왼쪽에서 오른쪽으로 단어를 읽고 다른 하나는 오른쪽에서 왼쪽으로 읽습니다. 그다음 일반적으로 타임 스텝마다 이 두 출력을 연결합니다. 이를 양방향 순환층(bidirectional recurrent layer)이라 합니다.

 

구현은 순환층을 keras.layers.Bidirectional로 감싸면됩니다.

keras.layers.Bidirectional(keras.layers.GRU(10, return_sequences=True))

 

NOTE_

더보기

Bidirectional 층은 GRU 층을 복사합니다.(반대 방향으로) 그 다음 두 층을 실행하여 그 출력을 연결합니다. GRU 층이 10개의 유닛을 가지면 Bidirectional 층은 타임 스텝마다 20개의 값을 출력합니다.

 

 

빔 검색

 

인코더-디코더 모델을 훈련하여 'comment vas-tu?'를 영어로 번역한다고 가정해봅시다. 안타깝게도 'How are you?'가 아닌 'How will you?'가 출력됩니다. 훈련 세트를 들여다 보니 'How will you play?'라는 뜻의 'Comment vas-tu jouer?'와 같은 문장이 많다는 걸 알았습니다. 스텝마다 무조건 가장 가능성 있는 단어를 출력해서는 최적의 번역을 만들지 못합니다. 널리 사용하는 방법 중 하나가 빔 검색(beam search)입니다.

 

beam search는 k개의 가능성 있는(가령, 상위 3개) 문장의 리스트를 유지하고 디코더 단계마다 이 문장의 단어를 하나씩 생성하여 가능성 있는 k개의 문장을 만듭니다. 파라미터 k를 beam width라고 부릅니다.

 

예를 들어, beam width 3으로 빔 검색을 한다고 했을 때, 'Comment vas-tu?'를 번역한다고 합시다. 첫 번째 디코더 스텝에서 모델이 어휘 사전에 있는 모든 단어에 대한 추정 확률을 출력할 것입니다. 최상위 3개 단어를 How(75%), What(3%), You(1%)라고 가정합시다. 이것이 현재 리스트입니다.

 

그 다음 세 개의 모델로 각 리스트를 복사해 각 문장의 다음 단어를 찾습니다. 각 모델은 어휘 사전에서 각 리스트의 다음 단어에 해당하는 추정 확률을 출력하는데, 첫 번째 모델은 'How' 문장에 이어질 단어를 찾습니다. will(36%), are(32%), do(16%)의 확률을 출력합니다. 실제 이 값은 How로 시작하는 문장이 주어졌을 때의 조건부 확률(conditional probability)입니다. 두 번째 모델은 What 문장을 이어가는 식입니다.

 

어휘 사전에 10,000개의 단어가 있다고 하면 각 모델은 10,000개의 단어에 대한 조건부 확률을 출력할 것입니다. 그 다음 모델이 두 단어로 이루어진 30,000(3x10,000)개의 문장에 대해 확률을 계산합니다. 예를 들어 첫 단어가 'How'(75%)고, 단어 'will'의 추정된 조건부 확률이 36%라면 'How will' 문장의 추정 확률은 75%*36% = 27%이 됩니다.

 

두 단어로 이루어진 문장 30,000개의 확률을 계산한 후 최상위 3개만 추립니다. 이들은 모두 How 단어로 시작할 것인데, 'How will'(27%), 'How are'(24%), 'How do'(12%)입니다. 'How are'이 아직 배제되지는 않았습니다.

 

그 다음 동일한 과정으로 모델 셋을 이용해 다음 단어를 예측합니다. 그 다음 세 단어로 이루어진 문장 30,000개의 확률을 계산합니다. 가령 가장 높은 확률을 가진 세 문장은 'How are you'(10%), 'How do you'(8%), 'How will you'(2%)입니다. 

 

추가적인 훈련 없이 사용법을 개선함으로써 인코더-디코더 모델의 성능을 높였습니다.

 

텐서플로 애드온을 사용하면 빔 검색을 아주 쉽게 구현할 수 있습니다.

beam_width = 10
decoder = tfa.seq2seq.beam_search_decoder.BeamSearchDecoder(
    cell=decoder_cell, beam_width=beam_width, output_layer=output_layer
)
decoder_initial_state = tfa.seq2seq.beam_search_decoder.tile_batch(
    encoder_state, multiplier=beam_width
)
outputs, _, _ = decoder(
    embedding_decoder, start_tokens = start_tokens, end_token=end_token,
    initial_state=decoder_initial_state
)

 

먼저 모든 디코더 셀(여기서는 10개)을 감싼 BeamSearchDecoder를 만듭니다. 그 다음 디코드를 위해 인코더의 마지막 상태를 복사합니다. 시작과 종료 토큰과 함께 이 상태를 디코더에게 전달합니다. 

 

이런 작업을 통해 꽤 짧은 문장에 대해서는 좋은 번역을 얻을 수 있습니다. (특히 사전훈련된 단어 임베딩을 사용한다면) 하지만, 긴 문장에 대해서는 성능이 매우 나쁜데, RNN의 제한된 단기 기억으로 인한 문제가 대두됩니다. 어텐션 매커니즘은 이 문제를 해결하는 혁신적인 방법입니다.

 

 

어텐션 메커니즘

 

그림 16-3

 

위 그림에서 milk를 lait으로 번역하려면 여러 단계를 이동해야합니다. 경로를 짧게 만들기 위해(?) 바흐다나우 어텐션(Bahdanau attention)은 각 타임 스텝에서 (인코더로) 인코딩하여 적절한 단어에 디코더가 초점을 맞추도록 합니다.

 

 

인코더에서 디코더로 은닉 상태를 보낼 때, 마지막 은닉 상태만 보내는 것이 아니라 인코더의 모든 출력을 디코더로 전송합니다. 각 타임 스텝에서 디코더의 메모리 셀은 받은 인코더의 모든 출력의 가중치 합을 계산합니다. 이 가중치가 집중할 단어를 결정합니다.

 

$\alpha _{\left(t,\ i\right)}$ 가중치는 $t$번째 디코더 타임 스텝에서 $i$번째 인코더 출력의 가중치입니다. 가령, $\alpha _{\left(3,\ 2\right)}$가 $\alpha _{\left(3,\ 0\right)}$, $\alpha _{\left(3,\ 1\right)}$보다 훨씬 크다면 'milk'에 더 많은 주의를 기울입니다. 

 

디코더는 매 타임 스텝마다 가중치가 반영된 입력과 이전 타임 스텝의 은닉 상태를 받습니다. 그림에는 안 나와있지만 이전 타임 스텝의 타깃 단어를 받습니다. (추론 시에는 이전 타임 스텝의 출력이 됩니다.)

 

$\alpha _{\left(t,\ i\right)}$ 가중치는 정렬 모델(alignment model or attention layer)이라는 작은 신경망에서 생성됩니다. 이 모델은 인코더-디코더와 함께 훈련됩니다. 정렬 모델은 TimeDistributed 클래스를 적용한 하나의 뉴런으로 구성된 Dense 층으로 시작합니다. 이 층은 인코더의 각 출력이 디코더의 이전 은닉 상태(그림에서 $h_2$)와 얼마나 잘 맞는지 점수($e_{\left(3,\ 2\right)}$)를 측정합니다. 마지막으로 각 인코더의 점수들을 소프트맥스 층에 통과시켜 각 인코더 출력에 대한 최종 가중치를 얻습니다. (소프트맥스 층은 타임 스텝에 각각 적용되지 않습니다.)

 

바흐다나우 어텐션은 연결 어텐션(concatenative attention) 또는 덧셈 어텐션(additive attention)이라고도 부릅니다.

 

NOTE_

더보기

입력 문장이 $n$개의 단어로 이루어져있고 출력 문장이 비슷한 길이라면 이 모델은 $n^2$개의 가중치를 계산해야 합니다. 보통 긴 문장이라고 해도 1000개 이상을 갖지 않아서 이정도 복잡도는 감내할만 합니다.

 

루옹 어텐션(Luong attention)은 인코더의 출력 하나와 이전 은닉 상태 사이의 유사도를 측정하는 대신, 간단히 두 벡터 사이의 점곱을 제안했습니다. 이 또한 좋은 유사도 측정 방법이고, 점곱은 매우 빠르게 계산할 수 있습니다. 때문에 곱셈 어텐션(multiplicative attention)이라고도 불립니다. 이렇게 하려면 두 벡터는 동일한 차원을 가져야합니다.

 

루옹 저자가 제안한 또 다른 간소화 버전은 디코더의 이전 타임 스텝($h_{\left(t-1\right)}$)이 아닌 현재 타임 스텝($h_{\left(t\right)}$)의 은닉 상태를 사용하는 것입니다. 그 다음 어텐션 메커니즘의 출력($\tilde{h_{\left(t\right)}}$)을 사용하여 바로 디코더 예측을 계산합니다.

 

여기에 더해, 일반 점곱 방법은 인코더의 출력이 점곱 계산 전에 먼저 선형 변환(즉, 편향이 없는 TimeDistributed가 적용된 Dense 층)을 통과하는 점곱 매커니즘의 변종을 제안했습니다. 일반 점곱 방법은 연결 어텐션보다 성능이 좋다는 것이 알려졌습니다.

 

[어텐션 메커니즘 식]

$$\tilde{h_{\left(t\right)}}=\sum _t^{\ }\alpha _{\left(t,\ i\right)}y_{\left(i\right)}$$

$$\alpha _{\left(t,\ i\right)}=\frac{\exp \left(e_{\left(t,\ i\right)}\right)}{\sum _{i`}^{\ }\exp \left(e_{\left(t,\ i`\right)}\right)}$$

$$ e_{\left(t,\ i\right)}\begin{cases}h_{\left(t\right)}^Ty_{\left(i\right)}&점곱\\h_{\left(t\right)}^TWy_{\left(i\right)}&일반\ 점곱\\v^T\tanh \left(W\left[h_{\left(t\right)};y_{\left(i\right)}\right]\right)&연결\end{cases}$$

 

텐서플로 애드온을 사용해 인코더-디코더 모델에 루옹 어텐션을 추가하는 방법입니다.

 

import tensorflow_addons as tfa

attention_mechanism = tfa.seq2seq.attention_wrapper.LuongAttention(
    units, encoder_state, memory_sequence_length=encoder_sequence_length
)
attention_decoder_cell = tfa.seq2seq.attention_wrapper.AttentionWrapper(
    decoder_cell, attention_mechanism, attention_layer_size=n_units
)

간단히 디코더 셀을 AttentionWrapper 클래스로 감싸고 원하는 매커니즘을 지정합니다.(여기서는 루옹 어텐션)

 

 

비주얼 어텐션

 

어텐션 메커니즘을 신경망 기계 번역(Neural machine translation, NMT)외에 사용된 어플리케이션에는 visual attention을 사용한 이미지 캡션(caption) 생성입니다.

 

먼저 합성곱 신경망이 이미지를 처리해 일련의 특성맵을 출력합니다. 그 다음 어텐션 매커니즘이 각각의 특성맵을 한 장에 한 단어씩 캡션을 생성합니다.

 

예를 들어 모델이 'A woman is throwing a frisbee in a park'를 생성했다면, frisbee를 출력할 때 원반에 초점을 맞춤을 알 수 있습니다.

 

 

설명 가능성

 

어텐션 매커니즘은 모델이 왜 그런 출력을 이끌어 내는지 이해하기 쉽다는 장점이 있습니다. 즉, 설명 가능성(explainability)이 좋습니다. 예를 들어 '눈 위를 걷는 강아지' 사진에서 '눈 위를 걷는 늑대'라는 캡션이 생성된다면, 거꾸로 돌아가 '늑대'라는 단어를 출력한 특성맵을 보며 초점이 어디에 있는지 살펴볼 수 있습니다.

 

강아지 뿐만 아니라 눈(내리는 눈)에 초점이 있을지도 모릅니다. 이는 늑대와 강아지를 구별하기 위해 눈이 주변에 많이 내렸는지를 확인했을 수도 있기 때문인데, 이런 문제를 눈 속에 있는 강아지 사진과 눈 속에 있지 않은 늑대 사진을 학습시킴으로써 해결할 수 있습니다.

 

어텐션 매커니즘은 매우 강력해서 어텐션 매커니즘만으로도 최고 수준의 모델을 만들 수 있습니다.

 

 

트랜스포머 구조: 어텐션이 필요한 전부다

 

 

Transformer 구조는 순환층, 합성곱 층을 전혀 사용하지 않고 어텐션 매커니즘만(그리고 임베딩 층, 밀집 층, 정규화 층, 몇 가지 다른 구성 요소)을 사용해 NMT 문제에서 최고 수준 성능을 크게 향상했습니다.

 

  • 왼쪽 부분이 인코더입니다. 앞에서와 같이 단어 ID의 시퀀스를 배치로 입력 받습니다. 입력 크기는 [배치 크기, 입력 문장 최대 길이]입니다. 인코더는 각 단어를 512차원로 출력합니다. 인코더의 출력 크기는 [배치크기, 입력 문장 최대 길이, 512]입니다. 위치 인코딩 윗쪽 부분이(박스 말하는 듯?) N번 반복되어 쌓아 올려집니다.(논문에서 N=6)
  • 오른쪽 부분이 디코더입니다. 훈련하는 동안 단어 ID의 시퀀스로 표현된 타깃 문장을 입력으로 받습니다. 또한 인코더의 출력을 받습니다. 디코더의 윗부분도 N번 반복되어 쌓아 올려집니다. 인코더의 최종 출력이 N개의 디코더 모두에 주입됩니다. 앞에서와 같이 타임 스텝마다 디코더는 다음 단어에 대한 확률을 출력합니다. 디코더의 출력 크기는 [배치 크기, 출력 문장의 최대 길이, 어휘 사전 길이]입니다. 추론 시에는 앞에서와 마찬가지로 이전 타임 스텝에서의 출력 단어를 주입합니다.
  • 대부분의 구성 요소는 알고 있는 것들입니다. 그림 전체에서 임베딩 층 2개, 스킵 연결 5xN개, 그 뒤를 따르는 정규화 층, 밀집층 2개(첫 번째는 ReLU, 두 번째는 None)로 구성된 피드포워드 모듈이 2xN개 있습니다. 마지막으로 출력층은 softmax 활성화 함수를 사용하는 밀집층입니다. 이 모든 층은 타임 스텝에 독립적(time-distributed)입니다. 따라서 각 단어는 다른 모든 단어에 대해 독립적으로 처리됩니다. 한 번에 한 단어씩 보면서 문장을 처리할 수 있는 구성요소가 필요합니다.
    • 인코더의 멀티-헤드 어텐션(multi-head attention) 층은 한 문장안에 있는 각 단어와 다른 나머지 단어 간의 관련 정도를 인코딩합니다. 예를 들어 'They welcomed the Queen of the United Kingdom'에서 'Queen'이라는 단어에 대해 'They', 'welcome'보다 'United', 'Kingdom'에 더 주의를 기울입니다. 이런 어텐션 메커니즘을 self-attention이라고 합니다. 디코더의 마스크드 멀티-헤드 어텐션(masked multi-head attention) 층도 동일한 작업을 수행합니다. 각 단어는 그 단어 이전에 등장한 단어에만 관련도를 구할 수 있습니다. 디코더의 윗쪽 masked multi-head attention은 디코더가 입력 문장에 있는 단어에 주의를 기울이는 곳입니다. 가령 디코더가 입력 문장 중 'Queen'에 주의를 가장 많이 기울일 것입니다. 
    • 위치 인코딩(positional encoding)은 문장에 있는 단어의 위치를 나타내는 단어 임베딩과 매우 유사한 단순한 밀집 벡터입니다. n번째 위치 인코딩이 n번째 단어의 단어 임베딩에 더해집니다. 이를 통해 모델이 각 단어의 위치를 알 수 있습니다. multi-head attention은 단어 사이의 관계만 보고 순서, 위치를 고려하지 않기 때문입니다. multi-head 층 외에 다른 층도 time-distributed해서 각 단어의 위치를 알 수 없기에 위치를 트랜스포머에게 어떻게든 전달하는 데 위치 인코딩이 사용됩니다.

트랜스포머의 새로운 두 구성 요소를 자세히 살펴보겠습니다.

 

 

위치 인코딩

 

여러가지 주기의 sine, cosine 함수로 정의한 고정된 위치 인코딩 방법을 소개합니다. $i$번째 위치 인코딩이 문장에 있는 $i$번째 단어의 단어 임베딩에 더해집니다. 위치 인코딩 행렬 $P$는 아래 식과 같이 정의되어 있습니다. $P_{p,\ i}$는 문장에서 $p$번째 위치에 있는 단어를 위한 인코딩의 $i$번째 원소입니다.

 

 

[사인/코사인 위치 인코딩]

$$ P_{p,\ 2i}=\sin \left(\frac{p}{10000^{\frac{2i}{d}}}\right) $$

$$ P_{p,\ 2i+1}=\cos \left(\frac{p}{10000^{\frac{2i}{d}}}\right) $$

 

이 방법은 모델을 사용해 학습된 위치 임베딩과 동일한 성능을 내면서도 임의의 긴 문장으로 확대할 수 있기 때문에 많이 사용됩니다. 위치마다 고유한 위치(절대 위치) 인코딩이 만들어집니다. [그림16-9]에서 수직선 (p=22)

 

선택에 따라 모델이 상대적인 위치도 학습할 수 있습니다. 아래 sine, cosine 곡선을 보면 38개 단어만큼 떨어진 두 단어는 (그림에서 p=22, p=60) i=100, i=101에서 항상 같은 위치 인코딩 값을 가집니다. 이는 같은 주기의 sine, cosine 함수를 사용해야하는 이유입니다. 즉, 사인 함수(i=100)만 사용하면 p=22, p=35(X 표시) 위치를 구별할 수 없습니다.

 

[?]

PositionalEmbedding 층은 텐서플로에서 제공하지는 않지만 만들기 어렵지 않습니다. 효율적인 이유로 생성자에서 위치 인코딩 행렬을 미리 계산합니다. 따라서 문장의 최대 길이 max_steps(i의 숫자인 듯), 각 단어를 표현할 차원수 max_dims를 알아야합니다.(?) 그 다음 call() 메서드에서 이 인코딩 행렬을 입력의 크기로 잘라 입력에 더합니다. 위치 인코딩 행렬을 만들 때 크기가 1인 첫 번째 차원을 추가했으므로 브로드캐스팅 규칙에 의해 이 행렬이 입력의 모든 문장에 더해집니다.

 

class PositionalEncoding(keras.layers.Layer):
  def __init__(self, max_steps, max_dims, dtype=tf.float32, **kwargs):
    super().__init__(dtype=dtype, **kwargs)
    if max_dims % 2 ==1: max_dims +=1  # max_dims는 짝수여야 합니다.
    p, i = np.meshgrid(np.arange(max_steps), np.arange(max_dims//2))
    pos_emb = np.empty((1, max_steps, max_dims))
    pos_emb[0, :, ::2] = np.sin(p/10000**(2*i/max_dims)).T
    pos_emb[0, :, 1::2] = np.cos(p/10000**(2*i/max_dims)).T
    self.positional_embedding = tf.constant(pos_emb.astype(self.dtype))
  def call(self, inputs):
    shape = tf.shape(inputs)
    return inputs + self.positional_embedding[:, :shape[-2], :shape[-1]]

 

그 다음 트랜스포머의 첫 번째 층을 만듭니다.

 

embed_size= 512; max_steps = 500; vocab_size=10000

encoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
decoder_inputs = keras.layers.Input(shape=[None], dtype=np.int32)
embeddings = keras.layers.Embedding(vocab_size, embed_size)
encoder_embeddings = embeddings(encoder_inputs)
decoder_embeddings = embeddings(decoder_inputs)
positional_encoding = PositionalEncoding(max_steps, max_dims=embed_size)
encoder_in = positional_encoding(encoder_embeddings)
decoder_in = positional_encoding(decoder_embeddings)

 

 

스케일 점-곱 어텐션

 

self attention은 한 단어와 나머지 다른 단어 사이에 관련도를 구한다. 이 관련도는 softmax 층을 통과시킨 결과, 즉 확률로 attention score라고 한다. 예를 들어 '나는 자연어 처리를 즐겨 한다'라는 말에서 '자연어'라는 단어와 다른 단어들 사이의 어텐션 스코어는 아래 표처럼 구해진다. 

 

나는     자연어 자연어     자연어 처리를    자연어 즐겨한다    자연어
Dot product Dot product Dot product Dot product
0.3 0.3 0.25 0.15

 

각 어텐션 스코어와 각 단어 임베딩을 각각 곱해 더해주면 단어('자연어')에 대한 컨텍스트(맥락) 벡터가 나오게 된다. 이를 모든 단어에 대해 수행해준다. self attention은 문법, 맥락을 이해하는데 도움을 준다.

 

 

scaled dot-product attention은 self-attention과 매우 유사한데, query가 self attention 예제에서 '자연어'와 같이 관계를 알아볼 단어이고, key, value가 다른 단어들이다. key=value이다.

 

스케일드 내적 어텐션 구조, https://simpling.tistory.com/3?category=364623

 

query, key, value는 모두 단어이고 벡터로 되어있는데, 단어들이 모인 문장은 행렬로 되어있는 구조이다.

 

https://velog.io/@cha-suyeon/%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%93%9C-%EB%8B%B7-%ED%94%84%EB%A1%9C%EB%8D%95%ED%8A%B8-%EC%96%B4%ED%85%90%EC%85%98Scaled-dot-product-Attention

 

self attention 처럼 Q, K를 통해 어텐션 스코어를 구합니다. 어떤 Q와 K가 중요한 역할을 하고 있다면 내적 값이 커집니다. 내적값이 커지면 벡터 공간 상 위치가 가까워집니다. (첫 번째 MatMul층) 그 다음 벡터의 차원이 커지면 학습이 잘 안될 수 있기 때문에 Scaling을 해줍니다.(두 번째 Scale층)

 

Query/Key 나는 자연어 처리를 즐겨한다
나는 23 X X X
자연어 16 27 X X
처리를 14 20 23 X
즐겨한다 12 19 20 23

 

전체 문장을 행렬로 입력하기 때문에, 자신보다 뒤에 있는 단어를 참고해 단어를 예측할 수 있기 때문에 masking층(3번째)을 통해 참고하지 못하도록 마스킹합니다. 그 후 softmax 층을 통과시켜 확률로 바꾸어주고, Value와 행렬곱을 해서 컨텍스트 벡터를 출력합니다.

 

$$ Attention(Q,K,V) = C^{T}V =Softmax(\frac{QK^{T}} {\sqrt{d_{k}}})V $$

 

$d_{k}$는 Q 혹은 K의 차원 수입니다.

 

keras.layers.Attention 층은 배치에 있는 여러 문장을 효율적으로 적용하는 scaled dot-product attention을 구현합니다.  이 층의 입력은 [배치 차원, Q or K or V의 차원]과 같습니다. 

 

Tip

더보기

텐서플로에서 A와 B의 크기가 [2, 3, 4, 5]와 [2, 3, 5, 6] 같이 2차원 이상의 텐서라면 tf.matmul(A, B)는 이 텐서를 각 원소에 행렬이 들어 있는 2x3 배열처럼 다루어 해당하는 행렬을 곱합니다. A에 있는 i번째 행과 j번째 열의 행렬이 B에 있는 i번째 행과 j번째 열의 행렬과 곱해집니다. 4x5 행렬과 5x6 행렬을 곱하면 4x6 행렬이 되므로 tf.matmul(A, B)의 결과는 [2, 3, 4, 6] 크기의 배열입니다.

 

numpy의 matmul() 함수도 동일한 결과를 만듭니다. 넘파이 dot() 함수와 같은 텐서플로 함수는 tensordot()입니다. tensordot() 함수는 3번째 매개변수에 점곱이 수행될 차원을 전달해야 합니다. 이 예에서는 첫 번째 행렬의 4번째 차원과 두 번째 행렬의 4번째 차원입니다. 따라서 np.dot(A, B) == tf.tensordot(A, B, (3, 2))입니다. 결과 행렬의 차원은 [2, 3, 4, 2, 3, 6]입니다.

 

 

멀티-헤드 어텐션을 스케일드 점-곱 어텐션으로 바꾸고 스킵 연결, 층 정규화, 피드포워드 모듈을 무시하면 트랜스포머 모델의 나머지 부분은 아래와 같이 구현됩니다.

 

Z = encoder_in
for N in range(6):
  Z = keras.layers.Attention(use_scale=True)([Z, Z])

encoder_outputs = Z
Z = decoder_in
for N in range(6):
  Z = keras.layers.Attention(use_scale=True, causal=True)([Z, Z])
  Z = keras.layers.Attention(use_scale=True)([Z, encoder_outputs])

outputs = keras.layers.TimeDistributed(keras.layers.Dense(vocab_size, activation='softmax'))(Z)

 

use_scale=True로 지정하면 파라미터를 추가해서 어텐션 스코어의 스케일을 적절히 낮추는 법을 배웁니다. 항상 동일한 인자($d_{k}$)로 어텐션 스코어를 낮추는 트랜스포머 모델과는 다릅니다. 두 번째 어텐션 층을 만들 때 causal=True로 지정하면 출력 토큰은 미래 토큰이 아니라 이전 출력 토큰에만 주의를 기울입니다.

 

 

멀티-헤드 어텐션

 

https://simpling.tistory.com/4

 

multi-head attention은 scaled dot-product attention 층의 묶음입니다. 각 층은 값, 키, 쿼리가 먼저 선형 변환됩니다.(즉, 활성화 함수가 없는 타임 스텝에 독립적인 Dense 층) 여기서 Query, key의 차원을 맞출 수 있습니다. 출력은 모두 단순히 연결되어 타임 스텝에 독립적인 선형 변환을 통과합니다. 여기서 attention score의 차원을 조절할 수 있습니다.

 

단어 played에서 과거형 등 번역에 필요한 특징처럼 단어에는 많은 특징들이 포함될 수 있습니다. Scaled Dot-Product 전에 V, K, Q에 선형 변환을 적용함으로써 한 선형층은 이 단어가 동사라는 정보만 남는 하나의 부분 공간으로 투영하고, 또 다른 선형층은 과거형이라는 사실만 추출하는 식입니다. scaled dot-product attention 층이 룩업 단계를 구현하고 마지막으로 모들 겨로가를 연결해 원본 공간으로 다시 투영합니다.

 

multi-head의 핵심은 query, key, value를 헤드 수만큼 나누어 병렬로 계산해주는 것이 핵심입니다. https://homl.info/transformertuto에서 트랜스퍼 모델을 구축하는 텐서플로 튜토리얼을 참고할 수 있습니다. 

 

 

언어 모델 분야의 최근 혁신

 

2018년은 'NLP를 위한 이미지넷 시대'라고 불릴 정도로 놀라운 속도로 발전했습니다. 갈 수록 큰 LSTM과 트랜스포머 기반의 모델이 엄청난 데이터셋에서 훈련되었습니다. 아래 추천 논문들을 소개합니다.

 

  • ELMo는 언어 모델 기반 임베딩(embeddings from language model)입니다. 심층 양방향 언어 모델의 내부 상태에서 학습된 문맥이 반영된 단어 임베딩입니다. 가령, queen이란 단어는 'Queen of the United Kingdom'과 'queen bee'에서 같은 임베딩을 갖지 않습니다.
  • ULMFiT는 NLP 작업을 위한 비지도 사전훈련의 효과를 설명했습니다. 저자들은 대용량 데이터셋에서 자기 지도 학습(가령, 데이터에서 자동으로 레이블을 생성하는 등)을 사용해 LSTM 언어 모델을 훈련했습니다. 그 다음 여러 작업에서 세부 튜닝을 수행했는데, 텍스트 분류 작업의 특정 부분에서 오차율이 최고로 적었습니다.
  • 파라미터 개수가 매우 많고 트랜스포머와 유사한 구조를 사용한 GPT-2 논문은 세부 튜닝 없이 여러 작업에서 좋은 성능(텍스트 분류, entailment(수반 관계), 유사도, 질문 대답 등)을 달성할 수 있었습니다. 이를 제로-샷 학습(zero-shot learning(ZSL))이라고 부릅니다.

 

BERT 논문에서도 대규모 텍스트 데이터에서 자기 지도 사전훈련의 효과가 있음을 보였습니다. GPT와 비슷한 구조이지만, 마스크가 없는 multi-head attention 층을 사용합니다. 이는 모델이 자연스럽게 양방향성(앞, 뒤 단어를 모두 볼 수 있음)을 갖습니다. 그래서 이름이 BERT(bidirectional encoder representations from transformers)입니다. 저자들은 BERT 모델의 강점을 잘 설명하는 2가지 사전훈련 작업을 제안했습니다.

 

  • 마스크드 언어 모델(Masked Language Model, MLM)

문장에 있는 각 단어는 15% 확률로 마스킹(선택)합니다. 모델은 마스킹된 단어를 예측하도록 훈련됩니다. 조금 더 정확히 말해 선택된 단어는 80% 확률로 마스킹되고, 10% 확률로 다른 단어로 바뀌고(세부튜닝하는 동안에는 <mask> 토큰이 없기 때문에 사전 훈련과 세부 튜닝 사이의 차이점을 줄이기 위해서), 10% 확률로 그대로 남겨집니다.(정확한 답으로 모델을 편향시키기 위해서)

 

 

 

  • 다음 문장 예측(Next Sentence Prediction, NSP)

두 문장이 연속적인지 아닌지 예측하도록 모델을 훈련합니다. 가령, 'The dog sleeps'와 'It snores loudly'는 연속적인 문장으로 예측해야하지만, 'The Earth orbits the Sun'과는 연속성이 없는 문장으로 예측해야합니다. 질문 대답이나 수반 관계와 같은 작업에서 세부 튜닝할 때 모델의 성능을 크게 향상시킵니다.

 

 

2018, 2019년의 주요 혁신은 더 나은 부분 단어(subword) 토큰화, 트랜스포머, 자기 지도 학습을 사용한 사전 훈련된 범용 언어 모델, 구조적 변경이 적은 세부 튜닝입니다.

 

다음 장에서는 오토인코더를 사용해 비지도 방식으로 심층 표현을 학습하는 방법을 설명합니다. 생성적 적대 신경망(GAN)을 사용해 이미지 등을 생성해봅시다!

 

 


출처: 핸즈온 머신러닝 2판

참고: https://simpling.tistory.com/3?category=364623
사진 출처: 핸즈온 머신러닝 2판