오늘은 autoregressive model을 다루려고 한다.
5.1은 자기회귀모델에 관한 비유이므로 넘어가겠다 !
목차는 다음과 같다.
5.2 LSTM 네트워크
5.3 RNN 확장
5.4 PixelCNN
오늘은 4장에서 배웠던 GAN에 비해 비교적 간단하니 집중해서 빠르게 정리해보자!
LSTM은 RNN(Recurrent neural network, 순환신경망)의 한 종류이다.
LSTM에 대해 설명하기에 앞서 RNN에 대한 간단한 설명을 해보자면
RNN은
- 순차 데이터를 처리하는 순환 층(RNN에서는 cell이라고 부름)을 이용해 특정 time step에서 출력한 셀의 출력을 다음 time step 입력에 사용한다.
(tanh 함수 이용 -> 결과가 [-1,1])
- gradient vanishing(그레디언트 감소) 문제 발생 ->sequence가 긴 데이터에는 잘 맞지 않음
기존 RNN에서 gradient vanishing 문제를 보완하기 위해 LSTM 을 개발했다.
기존에 우리가 다루던 이미지 데이터셋과 비교했을 때 텍스트 데이터셋의 특징을 살펴보자.
다음은 토큰화 코드이다.
# 구두점을 분리하여 별도의 '단어'로 취급합니다.
def pad_punctuation(s):
#->이 표현식은 string.punctuation에 있는 모든 구두점 문자 주위에 앞뒤로 공백을 추가합니다.
# ([{string.punctuation}]): string.punctuation 내의 모든 문자를 대괄호 [ ] 안에 넣어 문자 집합을 생성하고, 이를 캡처 그룹 ( )으로 감싸 캡처합니다.
# r" \1 ": \1는 첫 번째 캡처 그룹에 해당하는 문자를 참조합니다. 즉, 각 구두점 문자 양쪽에 공백을 추가하도록 합니다.
s = re.sub(f"([{string.punctuation}])", r" \1 ", s)
# re.sub(" +", " ", s): 이 표현식은 연속된 하나 이상의 공백을 하나의 공백으로 대체합니다.
# 여기서 +는 "하나 이상"을 의미하는 정규 표현식 퀀티파이어입니다.
#->중복 공백을 정리.
s = re.sub(" +", " ", s)
return s
text_data = [pad_punctuation(x) for x in filtered_data]
# 텐서플로 데이터셋으로 변환하기
text_ds = (
tf.data.Dataset.from_tensor_slices(text_data)
.batch(32)
.shuffle(1000)
)
# 벡터화 층 만들기
vectorize_layer = layers.TextVectorization(
standardize="lower", #텍스트 소문자로 변환
max_tokens=10000, output_mode="int", #가장 자주 등장하는 10,000개에 단어에 정수 부여
output_sequence_length=200 + 1, #시퀀스 길이가 201개의 토큰이 되도록 1. 자르기 2. 패딩 진행
)
# 훈련 세트에 층 적용
vectorize_layer.adapt(text_ds)
vocab = vectorize_layer.get_vocabulary()

윗쪽 출력은 TextVectorization을 적용한 결과이다.
아랫쪽 출력은 해당 문장을 토큰화한 결과이다.
단어의 시퀀스가 주어지면 다음 단어를 예측하도록 전체 시퀀스 중 한 토큰을 이동시킴.
# 레시피와 한 단어 이동한 동일 텍스트로 훈련 세트를 만듭니다.
def prepare_inputs(text):
text = tf.expand_dims(text, -1)
tokenized_sentences = vectorize_layer(text)
x = tokenized_sentences[:, :-1]
# 이 코드는 변환된 토큰화된 문장들에서 마지막 토큰을 제외한 모든 토큰을 포함하는 새로운 배열 x를 생성합니다.
# x는 모델이 입력으로 사용할 데이터로, 모델은 이 데이터를 바탕으로 다음 토큰을 예측하는 학습을 수행합니다.
y = tokenized_sentences[:, 1:]
# y는 x의 각 시퀀스보다 한 위치씩 뒤로 옮겨진 토큰을 포함합니다.
# 이는 x에 주어진 시퀀스 다음에 등장할 토큰, 즉 라벨 데이터를 나타냅니다.
# 모델은 이 y 데이터를 정답으로 사용하여 예측의 정확성을 학습합니다.
return x, y
train_ds = text_ds.map(prepare_inputs) #입력 토큰과 동일하지만 한 토큰 이동된 벡터.
임베딩 층은 각 정수 토큰을 embedding size길이의 벡터로 변환하는 lookup table 이다.

(출처: https://blog.kakaocdn.net/dn/bt6eUK/btrq58ZGMlu/5z9Tu63GScTwhPVXBDxJmK/img.png)
LSTM 층의 핵심은 은닉층(hidden state)이다.
은닉층의 흐름은 시퀀스가 순환층에 주입된 과정인데 이에 대해서 자세히 이해해보자.
h_{t-1}(이전 은닉 상태) + x_t(현재 time step에서의 데이터) = h_{t}(현재 time step에서의 은닉 상태)
=>이전 층의 은닉 상태와 현재 time step에서의 데이터가 셀의 입력으로 들어가 현재 은닉 상태를 출력한다.
*이 그림에서 LSTM이 써진 박스가 cell이다.

(출처: https://wikidocs.net/images/page/152773/2.JPG)
inputs = layers.Input(shape=(None,), dtype="int32")
#토큰의 수와 임베딩 벡터의 차원을 임베딩 층에 input으로 줌.
x = layers.Embedding(10000, 100)(inputs)
#LSTM의 은닉층에는 은닉 벡터의 차원을 128개로 주고, 전체 타임 스텝의 은닉 상태 반환하도록 설정.
x = layers.LSTM(128, return_sequences=True)(x)
#각 타임 스텝의 은닉 상태를 토큰에 대한 확률 벡터로 지정.
outputs = layers.Dense(100000, activation="softmax")(x)
lstm = models.Model(inputs, outputs)
#SparseCategoricalCrossentropy는 다중 클래스 분류 문제에 자주 사용되는 손실 함수로,
#실제 레이블이 정수 형태로 제공될 때 적합
loss_fn = losses.SparseCategoricalCrossentropy()
lstm.compile("adam", loss_fn)
lstm.fit(train_ds, epochs=25)
여기서 토큰의 수와 시퀀스 길이에 대한 개념을 잠깐 정리해보자.
- 토큰의 수: 이는 주어진 데이터의 구성 단위인 토큰들의 총 개수를 의미합니다. 토큰은 단어, 문자 등입니다.
- 시퀀스 길이: 이는 LSTM 모델에 입력되는 시퀀스(데이터의 연속적인 열)의 길이를 의미합니다. 시퀀스 길이는 모델이 한 번에 처리할 수 있는 토큰의 수를 지정합니다. 자연어 처리에서 하나의 문장이나 문단을 모델에 입력할 때, 그 문장 또는 문단의 토큰 수가 시퀀스 길이가 됩니다.
예를 들어 토큰의 수가 10,000개일 때 시퀀스 길이가 200이라면 전체 토큰의 수는 10,000이고 한 번에 200개의 토큰을 처리한다는 의미이다.
아래 코드는 훈련 epoch이 끝날 때 텍스트를 생성하는 콜백함수이다.
# TextGenerator 체크포인트 만들기
class TextGenerator(callbacks.Callback):
def __init__(self, index_to_word, top_k=10):
self.index_to_word = index_to_word
self.word_to_index = { #1. 단어-> 토큰 역매핑
word: index for index, word in enumerate(index_to_word)
}
def sample_from(self, probs, temperature): #2. temeperature 매개변수 사용해 확률 업데이트
probs = probs ** (1 / temperature)
probs = probs / np.sum(probs)
return np.random.choice(len(probs), p=probs), probs
def generate(self, start_prompt, max_tokens, temperature):
start_tokens = [ #3. start_prompt는 생성과정을 시작하기 위해 모델에 제공하는 단어의 문자열. 단어는 토큰의 리스트로 변환된다.
self.word_to_index.get(x, 1) for x in start_prompt.split()
]
sample_token = None
info = []
while len(start_tokens) < max_tokens and sample_token != 0:
x = np.array([start_tokens])
y = self.model.predict(x, verbose=0) #4. 모델은 시퀀스의 다음에 나올 단어의 확률을 출력한다.
sample_token, probs = self.sample_from(y[0][-1], temperature) #5. 윗줄 코드서의 확률은 sample_from 메서드로 전달돼 temperature 기반으로 단어 선택.
info.append({"prompt": start_prompt, "word_probs": probs})
start_tokens.append(sample_token) #6. 생성과정 반복 위해 새로운 단어를 프롬프트 텍스트에 추가.
start_prompt = start_prompt + " " + self.index_to_word[sample_token]
print(f"\n생성된 텍스트:\n{start_prompt}\n")
return info
def on_epoch_end(self, epoch, logs=None):
self.generate("recipe for", max_tokens=100, temperature=1.0)
temperature 매개변수 값에 따른 학습의 차이를 보자.

LSTM 모델은 여러 맥락에 맞게 가장 가능성 있는 다음 단어의 분포를 생성할 수 있지만 단어의 의미를 모른다는 단점이 있다.
기존 LSTM의 층의 개수는 1개 -> 이 층을 여러 개 쌓으면 '적층 순환 네트워크'가 됨.
GRU(Gated Recurrent Unit)은 LSTM 셀과 2가지 차이점을 보인다.
1. 삭제 게이트 -> 리셋 게이트, 입력 게이트 -> 업데이트 게이트
2. 셀 상태와 출력 게이트 제거. 셀은 은닉 상태만 출력.

Bidirectional 층은 전체 텍스트를 바탕으로 추론하는 문제에서 사용하기 위해 만들어짐.
기존에 사용하던 forward 방향 & back 방향을 둘 다 사용
-> RNN이 time step의 앞뒤에서 모두 학습할 수 있다.
*GRU층의 은닉 상태에서의 셀 유닛의 개수는 한 방향 셀(기존에 사용하던) 유닛개수의 두 배이다.
다음은 PixelCNN에 대해 배워보겠다! 안뇽!