생성 AI - Ch5. autoregressive model(자기회귀 모델)

­문재원·2024년 5월 22일

genAI

목록 보기
4/5

오늘은 autoregressive model을 다루려고 한다.
5.1은 자기회귀모델에 관한 비유이므로 넘어가겠다 !

목차는 다음과 같다.

5.2 LSTM 네트워크
5.3 RNN 확장
5.4 PixelCNN

오늘은 4장에서 배웠던 GAN에 비해 비교적 간단하니 집중해서 빠르게 정리해보자!

5.2 LSTM 네트워크

LSTM은 RNN(Recurrent neural network, 순환신경망)의 한 종류이다.
LSTM에 대해 설명하기에 앞서 RNN에 대한 간단한 설명을 해보자면

RNN은

  • 순차 데이터를 처리하는 순환 층(RNN에서는 cell이라고 부름)을 이용해 특정 time step에서 출력한 셀의 출력을 다음 time step 입력에 사용한다.
    (tanh 함수 이용 -> 결과가 [-1,1])
    - gradient vanishing(그레디언트 감소) 문제 발생 ->sequence가 긴 데이터에는 잘 맞지 않음

기존 RNN에서 gradient vanishing 문제를 보완하기 위해 LSTM 을 개발했다.

5.2.2 텍스트 데이터 다루기

기존에 우리가 다루던 이미지 데이터셋과 비교했을 때 텍스트 데이터셋의 특징을 살펴보자.

  1. 텍스트 데이터는 이산적 데이터이므로 일반적으로 역전파를 적용할 수 없다.
  • 이미지 데이터셋: 이미지의 픽셀은 연속적인 스펙트럼 -> 역전파(back propagation) 적용 쉬움
  • 텍스트 데이터셋: 개별적인 데이터 조각(문자/단어)로 구성 -> 역전파 적용 어려움.
  1. 텍스트 데이터에는 시간 차원이 존재한다.
  • 이미지: 2개의 공간 차원 O, 시간 차원 X -> 동시에 처리 가능
  • 텍스트: 시간 차원 O, 공간 차원 X -> 순서에 의존성 O
  1. 텍스트 데이터는 개별 단위(단어/문자)의 변화에 민감
  2. 텍스트 데이터는 규칙 기반의 문법 구조 존재.

5.2.3 토큰화

  • 토큰화: 텍스트를 개별 단위(단어/문자)로 나누는 작업
    • 문자 토큰
    • 단어 토큰

다음은 토큰화 코드이다.

# 구두점을 분리하여 별도의 '단어'로 취급합니다.
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을 적용한 결과이다.
아랫쪽 출력은 해당 문장을 토큰화한 결과이다.

5.2.4 데이터셋 만들기

단어의 시퀀스가 주어지면 다음 단어를 예측하도록 전체 시퀀스 중 한 토큰을 이동시킴.

# 레시피와 한 단어 이동한 동일 텍스트로 훈련 세트를 만듭니다.
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) #입력 토큰과 동일하지만 한 토큰 이동된 벡터. 

5.2.6 임베딩 층

임베딩 층은 각 정수 토큰을 embedding size길이의 벡터로 변환하는 lookup table 이다.


(출처: https://www.google.com/url?sa=i&url=https%3A%2F%2Fwikidocs.net%2F33793&psig=AOvVaw1zopwe5ldZMgizoRe7Dlxk&ust=1716446973264000&source=images&cd=vfe&opi=89978449&ved=0CBIQjRxqFwoTCOi97rPVoIYDFQAAAAAdAAAAABAJ)

  • integer: 토큰의 번호를 의미
  • lookup table의 가로의 길이: 임베딩 크기(=embedding size)
  • lookup table의 새로의 길이: 토큰의 수
  • lookup 벡터: lookup table의 구성요소로, 모델에 의해 학습되는 가중치이다. 벡터의 총 개수는 가로x세로=(임베딩 크기)x(토큰의 수)

5.2.7 LSTM 층


(출처: 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이다.

5.2.8 LSTM 셀


(출처: https://wikidocs.net/images/page/152773/2.JPG)

각 gate의 역할

  • forget gate: 얼마나 잊어버릴지 결정. 즉 이전 셀 상태 C_{t-1}을 얼마나 유지할지 결정
  • input gate: 이전 셀 상태 C_{t-1}에 얼마나 새로운 정보를 추가할지 결정
  • output gate: 업데이트된 셀 상태 C_t를 셀의 출력으로 얼마나 보낼지 결정

단계별 흐름

https://wikidocs.net/152773

5.2.9 LSTM 훈련

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개의 토큰을 처리한다는 의미이다.

5.2.10 LSTM 분석

아래 코드는 훈련 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 매개변수 값에 따른 학습의 차이를 보자.

  • temperature=1.0인 경우
    • 더 모험적/ 다양함 -> 정확도 낮
  • temperature=0.2인 경우
    - 정확도 높다.
    • 첫 번째 선택 초큰에 훨씬 많은 가중치 부여 -> 다양성 적음.
    자! 이제 LSTM을 마무리하며 의의와 단점을 살펴보자.

LSTM 모델은 여러 맥락에 맞게 가장 가능성 있는 다음 단어의 분포를 생성할 수 있지만 단어의 의미를 모른다는 단점이 있다.

5.3. RNN의 확장

5.3.1 적층 순환 네트워크

기존 LSTM의 층의 개수는 1개 -> 이 층을 여러 개 쌓으면 '적층 순환 네트워크'가 됨.

5.3.2 GRU 층

GRU(Gated Recurrent Unit)은 LSTM 셀과 2가지 차이점을 보인다.
1. 삭제 게이트 -> 리셋 게이트, 입력 게이트 -> 업데이트 게이트
2. 셀 상태와 출력 게이트 제거. 셀은 은닉 상태만 출력.

5.3.3 양방향 셀

Bidirectional 층은 전체 텍스트를 바탕으로 추론하는 문제에서 사용하기 위해 만들어짐.
기존에 사용하던 forward 방향 & back 방향을 둘 다 사용
-> RNN이 time step의 앞뒤에서 모두 학습할 수 있다.

*GRU층의 은닉 상태에서의 셀 유닛의 개수는 한 방향 셀(기존에 사용하던) 유닛개수의 두 배이다.

5.4 PixelCNN

다음은 PixelCNN에 대해 배워보겠다! 안뇽!

profile
얼렁뚱땅 요리조리

0개의 댓글