Basic Deep Learning/Dive into Deep Learning 리뷰

[D2L] 8. RNN

needmorecaffeine 2022. 8. 8. 11:47

1. Statistical Basic

 

 이전까지 내용에서는 우리가 사용하는 데이터가 어떤 한 분포를 따르고 이 분포에서 추출된 것으로 가정했었다. 하지만 상식적으로 이렇게 특정 분포를 띄는 데이터는 흔치 않다. 글의 문단의 단어들의 경우 분포를 따르지 않고 오히려 그 sequence를 가지고 있을 확률이 높다. 글 뿐만 아니라 비디오의 이미지 프레임, 대화의 음성과 같은 데이터는 모두 sequence를 가지고 있을 확률이 높다.

 

 이 얘기를 한 이유는 이전의 CNN은 spatial information을 잘 다뤘다면 RNN(Recurrent Neural Network)은 sequential information을 잘 다루기 때문이다. RNN은 과거의 변수를 잘 저장하고 이를 최근의 input과 처리하여 ouput를 반환하는데 강점을 가진 모델 계열이다. 이러한 이유로 RNN은 NLP에서 자주 사용된다.

 

 RNN은 과거의 데이터를 기반으로 최근의 input을 다루는 모델이라고 하였는데 이를 수식으로 나타내면 다음과 같다. t를 예측해야 하는 시점이라고 한다면 P(Xt | Xt-1, Xt-2, ..... X1)를 계산할려고 하는 것이 RNN인 것이다. 하지만 이 때는 t에 따라 input의 갯수가 변하는다는 문제가 생긴다. 이를 해결하는 방법은 다음과 같다.

 

 1) Xt-1, Xt-2, ..... X1과 같이 길고 많은 input이 다 필요한 것이 아닌 timespan s를 정해두고 Xt-1, Xt-2, ..... Xt-s에 대해서만 input으로 사용하는 것이다. 이렇게 s를 설정해두면 input의 갯수는 항상 동일하고 이런 model들을 스스로 regression을 행한다는 점에서autoregressive 모델이라고 한다.

 

 2) 이전의 input에 대한 요약정보를 가지는 Ht를 계속 유지한다. 동시에 예측 Xt에 대해서도 계속 Ht를 업데이트한다. 

예측 Xt = P(Xt | Ht) 이고 Ht = g(Ht-1, Xt-1)로 표현할 수 있다. Ht는 관측된 데이터는 아니기에 이런 모델을 latent autoregressive model이라고 부른다.

 위 두가지 방법 모두 training data를 어떻게 만들어야 하는지의 질문이 생기는데 하나의 방법은 과거의 데이터로 그 다음에 등장할 데이터를 예측하는 것이다. 이 때 시간의 변화에 따라 데이터가 변하겠지만 공통적인 가정은 Xt라는 특정 값은 시간에 따라 변하더라고 dynamics of sequence 자체는 변하지 않는다는 것이다. 이런 가정이 있을 시 다음과 같은 표현이 가능하다. 아래 표현은 연속, 이산형 변수에 관계없이 모두 사용 가능하다.

 이전에 언급한 autoregressive 모델에서 우리는 과거의 모든 데이터가 아닌 Xt-1, Xt-2, ..... Xt-s만을 사용하는 autoregressive model에 대 얘기했다. 이러한 접근이 성립될 시 우리는 이 sequence를 Markov condition이라고 한다. s=1일 때, firt order Markov model이라고 한고 P(x)는 다음과 같다.

 특히 이산형 변수일 때 다음과 같은 chain 형태로도 표현 할 수 있다.

 물론 아래와 같이 반대로 더 최근의 데이터들을 먼저 가지고 미래 데이터를 예측하는 task도 생각해 볼 수 있다.

 위와 같이 역전된, 즉 이전 데이터들의 순서가 바뀐 조건부 확률로 데이터를 예측할 수 있지만 과거부터 현재라는 자연스러운 흐름을 무시할 필요는 없다. noise를 고려해보면 Xt+1 = f(Xt) + e 로 표현할 수 있는데 이는 과거에서 그 다음의 t+1 시점에 noise를 더할 수 있지만 t+1 시점에서 t를 볼 때 noise를 더할 수는 없다는 것도 역순으로 생각하는 것의 문제점이다.

 

 간단한 MLP를 통해 training을 해보자. 

먼저 sequence dataset을 만든다.

T = 1000
time = torch.arange(1, T+1, dtype = torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
d2l.plot(time, [x], 'time', 'x', xlim = [1, 1000], figsize = (6, 3))

 이 sequence data를 feature과 label의 형태로 바꾸어 모델이 훈련될 수 있게 한다. time span을 고려하여 Yt = Xt / Xt = [Xt-s, ..., Xt-1]로 나눈다. 

# Turn sequence into features and labels that model can train on
# map yt = xt / xt = [xt-s, ...., xt-1]
# pad the sequence with zero

# time span tau
tau = 4 
features = torch.zeros((T - tau), tau)
for i in range(tau):
  features[:, i] = x[i:T-tau+i]
labels = x[tau:].reshape((-1, 1))
# only use first 600 features
batch_size, n_train = 16, 600

train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train = True)

 간단한 MLP를 구성하고 훈련해본다.

def init_weights(m):
  if type(m) == nn.Linear:
    nn.init.xavier_uniform_(m.weight)

# simple mlp
def get_net():
  net = nn.Sequential(nn.Linear(4, 10),
                      nn.ReLU(),
                      nn.Linear(10, 1))
  net.apply(init_weights)
  return net

loss = nn.MSELoss(reduction = 'none')
def train(net, train_iter, loss, epochs, lr):
  trainer = torch.optim.Adam(net.parameters(), lr)
  for epoch in range(epochs):
    for X, y in train_iter:
      trainer.zero_grad()
      l = loss(net(X), y)
      l.sum().backward()
      trainer.step()
    print(f'epoch {epoch + 1}, '
              f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
    
net = get_net()
train(net, train_iter, loss, 5, 0.01)

 loss값과 예측 그래프를 보면 잘 예측하고 있는 것을 확인할 수 있다. 

하지만 여기서 생각해봐야 할 것이 tau 4, 즉 600개의 train data로 604개까지의 데이터를 예측하는 것 그 이상으로 예측할 경우는 어떻게 될지이다. 주어진 600개의 데이터로 604개의 데이터를 예측하고 그 이후의 데이터를 예측하는 것은 다음의 계속된 forward step들이 필요하다.

 이처럼 Xt+k를 time stpe t+k로 예측하는 것을 k-step-ahead prediction라고 부른다. 위 경우에서는 X604+k이다.

그렇다면 이제 multistep aheads를 해보자.

# k-step ahead prediction

multistep_preds = torch.zeros(T)
multistep_preds[: n_train + tau] = x[: n_train + tau]
for i in range(n_train + tau, T):
    multistep_preds[i] = net(
        multistep_preds[i - tau:i].reshape((1, -1)))

 위 그래프를 보면 multistep인 초록색 선이 실제 데이터에서 벗어나게 예측하고 있는 것을 볼 수 있다. 이런 이유는 error들이 계속해서 쌓이기 떄문이다. 한번의 step으로 error e1이 생겼다고 하고 이것을 input으로 step2을 진행할 시 e2 = e + e1*c로 상수 c가 곱해지면 forward연산을 할 때마다 error들이 급격히 커지게 되는 것이다. 24시간 후의 날씨를 예측하라는 task가 있다면 이는 비교적 잘 예측하지만 그 이상의 시간에 대해 예측해야 한다면 error는 점점 커지게 되는 이유가 이것이다.

 

 좀 더 다양한 multi step으로 비교해보면 다음과 같다.

max_steps = 64

features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# Column `i` (`i` < `tau`) are observations from `x` for time steps from
# `i + 1` to `i + T - tau - max_steps + 1`
for i in range(tau):
    features[:, i] = x[i: i + T - tau - max_steps + 1]
# Column `i` (`i` >= `tau`) are the (`i - tau + 1`)-step-ahead predictions for
# time steps from `i + 1` to `i + T - tau - max_steps + 1`
for i in range(tau, tau + max_steps):
  features[:, i] = net(features[:, i - tau:i]).reshape(-1)

 예상했는 step이 클수록 실제 데이터와 그 차이가 큰 것을 알 수 있다.


2. Text Preprocessing

 위에서 만든 예저 sequence data와 같이 대표적인 sequence data는 텍스트이다. 이 텍스트에 다루기 위한 전처리의 과정을 알아보자.

 1) 텍스트를 string형으로 메모리로 불러온다.

 2) string을 token으로 쪼갠다.

 3) vocabulary table을 구성해 쪼갠 token에 대해 숫자 인덱싱을 한다.

 4) 텍스트를 숫자 인덱싱으로 바꿔 모델 작업을 할 수 있게 한다.

 이 작업을 아래의 데이터셋으로 진행해보면 다음과 같다.

d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt',
                                '090b5e7e70c295757f55df93cb0a180b9691891a')
def read_time_machine(): 
    """Load the time machine dataset into a list of text lines."""
    with open(d2l.download('time_machine'), 'r') as f:
        lines = f.readlines()
    return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]
lines = read_time_machine()
print(f'# text lines: {len(lines)}')
print(lines[0])
print(lines[10])

# Tokeization
# split text lines into word or character tokens

def tokenize(lines, token = 'word'):
  if token == 'word':
    return [line.split() for line in lines]
  elif token == 'char':
    return [list(line) for line in lines]
  else:
        print('ERROR: unknown token type: ' + token)
tokens = tokenize(lines)
for i in range(11):
    print(tokens[i])

vocabulary를 만드는 class이다.

class Vocab: 
    """Vocabulary for text."""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                   reverse=True)
        # The index for the unknown token is 0
        self.idx_to_token = ['<unk>'] + reserved_tokens
        self.token_to_idx = {token: idx
                             for idx, token in enumerate(self.idx_to_token)}
        for token, freq in self._token_freqs:
            if freq < min_freq:
                break
            if token not in self.token_to_idx:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1
    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

    def unk(self):  # Index for the unknown token
        return 0

    def token_freqs(self):  # Index for the unknown token
        return self._token_freqs

def count_corpus(tokens): 
    """Count token frequencies."""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)
vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])

def load_corpus_time_machine(max_tokens=-1): 
    """Return token indices and the vocabulary of the time machine dataset."""
    lines = read_time_machine()
    tokens = tokenize(lines, 'char')
    vocab = Vocab(tokens)
    # Since each text line in the time machine dataset is not necessarily a
    # sentence or a paragraph, flatten all the text lines into a single list
    corpus = [vocab[token] for line in tokens for token in line]
    if max_tokens > 0:
        corpus = corpus[:max_tokens]
    return corpus, vocab
corpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)

3. Language Models

 위에서 학습한 텍스트 데이터를 token화 하는 것은 sequence of discrete observation이라고 할 수 있다. t개의 text sequence가 주어졌을 때 language model의 목표는 결확확률 P(X1, X2, ... , Xt)를 구하는 것이라 할 수 있다.

"deep learning is fun"이라는 문장을 예로 들면 다음과 결합확률 그리고 조건부 확률 식으로 표현할 수 있다. 이런 식의 표현을 Markov model이라고 한다.

 주어진 이전의 단어에 대한 조건부 확률값이 language model의 파라미터가 되는 것이다. 세부적으로 그 값 계산 방식을 보면 조건부 확률은 다음과 같이 계산할 수 있을 것이다.

 하지만 deep과 learning이 함께 쌍으로 발생하는 경우는 매우 적을 것이다. 특히 그 단어 조합이 흔치 않을 수도 있고 아예 없을 수도 있다. 이런 경우의 대안이 Laplae smoothing이다. 모든 count에 작은 상수를 더하는 것인데 n을 전체 단어의 갯수라 하고 m을 unique words의 갯수라고 하면 다음과 같은 작업이 이뤄진다.

 위에서의 e1, 2, 3가 하이퍼 파라미이고 e1이 0일 경우 smoothing이 일어나지 않는다. 하지만 위와 같은 방식도 문제가 있는데 먼저 모든 count를 저장해야 한다. 그리고 단어의 의미를 무시하게 된다. 또한 긴 단어의 sequence는 대부분 새로 등장하는 것이 많기 때문에 성능이 좋지 않을 수 있다.

 

 위에 들었던 Markov model의 예시와 같이 language modeling도 다음과 같은 표현이 가능하다. 

 다음과 같이 변수를 몇개를 포함하냐에 따라 n-gram으로 부른다.

위의 개념에 대해 이전에 사용한 데이터셋으로 살펴보자.

tokens = d2l.tokenize(d2l.read_time_machine())
# Since each text line is not necessarily a sentence or a paragraph, we
# concatenate all text lines
corpus = [token for line in tokens for token in line]
vocab = d2l.Vocab(corpus)
vocab.token_freqs[:10]

 빈도 수 기준 상위 10개의 단어를 보면 문장에서 매우 자주 쓰이는 단어들임을 알 수 있다. 이런 단어들을 stop words로 분류하는 경우가 많은데 이번에는 해당 작업을 하지는 않는다. 상위 10개 뿐만 아니라 빈도 수가 더 작은 단어들도 살펴보면 그 빈도수는 기하급수적으로 감소함을 알 수 있다.

 상위 몇개의 단어만을 제외하고 모든 단어들은 straight line on a log plot을 따르는데 이것은 단어가 Zipf's law를 따른다고도 하며 i번째로 빈도 수가 높은 단어의 빈도수 ni는 다음과 같은 수식을 따른다는 것이 이 law의 의미이다. 여기서 alpha는 분포의 특징을 나타내는 exponent이다.

 하나의 단어 단독으로는 위 law를 따른다는 것을 확인했다면 단어의 조합, 즉 n-gram은 어떻게 될까?

단어 단독, 즉 unigram과 bigram, trigram을 살펴보면 다음과 같다.

bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])]
bigram_vocab = d2l.Vocab(bigram_tokens)
bigram_vocab.token_freqs[:10]

trigram_tokens = [triple for triple in zip(
    corpus[:-2], corpus[1:-1], corpus[2:])]
trigram_vocab = d2l.Vocab(trigram_tokens)
trigram_vocab.token_freqs[:10]

 그래프를 보면 bigram, trigram 모두 unigram과 마찬가지로 더 작은 alpha로 Zipf's law를 따른다는 것을 확인할 수 있다. 또한 n에 따라 크게 다른 점을 가지지 않는다는 것도 확인할 수 있다.

 

 또한 긴 sequence data를 어떻게 읽을 것인지도 중요하다. 긴 sequence의 경우 model이 한번에 처리할 수 없기 때문에 어떻게 split할 거이냐 인데 neural network에 텍스트 데이터를 훈련할 때 사전 정의한 n step으로 minibatches of sequence를 불러와야 한다. n이 5일 때 아래의 예시를 보자.

 위 다섯가지 방식 중 어떤 방식으로 sequence를 분리하고 불러와야 할까? 위 다섯가지 방법 모두 좋다. 하지만 다섯가지 중 한가지만을 선택한다면 network에서 학습할 수 있는 coverage가 한정되어 있다. 일반적으로 말하자면 coverage와 randomness를 동시에 얻을 수 있는 random offset으로 수행해야 한다. 이제 이 random sampling과 sequential partitioning을 어떻게 할지 얘기해보자.

 

 1) Random Sampling

 아래의 코드로 데이터에서 minibatch를 만든다. batch_size는 # of sequence in each minibatch를, num_step은 각각의 subsequence에서의 사전 정의된 time step이다.

def seq_data_iter_random(corpus, batch_size, num_steps):
    """Generate a minibatch of subsequences using random sampling."""
    # Start with a random offset (inclusive of `num_steps - 1`) to partition a
    # sequence
    corpus = corpus[random.randint(0, num_steps - 1):]
    # Subtract 1 since we need to account for labels
    num_subseqs = (len(corpus) - 1) // num_steps
    # The starting indices for subsequences of length `num_steps`
    initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
    # In random sampling, the subsequences from two adjacent random
    # minibatches during iteration are not necessarily adjacent on the
    # original sequence
    random.shuffle(initial_indices)
    def data(pos):
        # Return a sequence of length `num_steps` starting from `pos`
        return corpus[pos: pos + num_steps]
    num_batches = num_subseqs // batch_size
    for i in range(0, batch_size * num_batches, batch_size):
    	# Here, `initial_indices` contains randomized starting indices for
		# subsequences
        initial_indices_per_batch = initial_indices[i: i + batch_size]
        X = [data(j) for j in initial_indices_per_batch]
        Y = [data(j + 1) for j in initial_indices_per_batch]
        yield torch.tensor(X), torch.tensor(Y)

 sequence 0 ~ 34 를 예시로 batch_size가 5인 경우 (35-1)/5 = 6개의 feature-label 쌍을 만들 수 있다. batch_size가 2인 경우는 3개의 minibatch만을 가진다. 확인해보자.

my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)

 

 2) Sequential Partitioning

 iteration 동안 가지는 두개의 인접한 minibatch는 original sequence에서도 인접합니다. 이런 방법은 minibatch iterating 동안 split subsequence split의 order를 보존할 수 있고 이런 이유로 Sequential Partitioning라고 불린다.

def seq_data_iter_sequential(corpus, batch_size, num_steps):  #@save
    """Generate a minibatch of subsequences using sequential partitioning."""
    # Start with a random offset to partition a sequence
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = torch.tensor(corpus[offset: offset + num_tokens])
    Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i: i + num_steps]
        Y = Ys[:, i: i + num_steps]
        yield X, Y

실제 original sequence에서 인접한 minibatch를 가져오는지 보자.

for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):
    print('X: ', X, '\nY:', Y)

 위 두개의 sampling function을 가지는 하나의 class는 다음과 같다.

class SeqDataLoader: 
    """An iterator to load sequence data."""
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps
    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

 이후 학습을 위한 다음 function도 정의하여 data iterator와 vocabulary를 반환하는 기능도 만들 수 있다.

def load_data_time_machine(batch_size, num_steps,  #@save
                           use_random_iter=False, max_tokens=10000):
    """Return the iterator and the vocabulary of the time machine dataset."""
    data_iter = SeqDataLoader(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab

4. RNN Basic

 이전의 n-gram model을 생각해보면 time step t때의 단어 Xt의 조거분 확률은 이전의 n-1개의 단어에만 의존한다. 만약 t-(n-1)이전의 time step의 단어들도 함께 고려하고 싶다면 n을 증가시켜야 한다. 하지만 이렇게 하면 parameter는 기하급수적으로 증가하고 vocabulary V의 V^n개의 단어를 저장해야 한다. 따라서 P(Xt | Xt-1, ... , Xt-n+1)을 modeling하는 것보다는 아래와 같은 latent variable을 사용하는 것이 더 좋다.

여기서에서 Ht-1은 hidden state로 time step t-1까지의 정보를 저장한다. 더 일반적으로 time step t의 hidden state는 최근 input Xt와 이전 hidden state Ht-1로 계산된다.

hidden state Ht는 이전까지 관찰된 모든 정보를 가지고 있기에 이 또한 연산과 저장이 매우 expensive할 수도 있다.

 

 여기서 헷갈릴 수 있는 것이 hidden layer와 hidden state인데, hidden layer는 input에서부터 output까지 숨은 layer를 의미하고 hidden state는 이전까지의 time step의 데이터를 보고 input을 작업해 다음 step으로 넘겨주는 것이다.

hidden layer를 MLP 기준 하나의 layer가 있다고 할 때, 복습해보면 다음의 연산을 거친다.

 hidden state는 위 과정과 완전 다르다. 현재의 time step에서 계산되는 hidden variable은 다음과 같다.

 X는 minibatch input이고 Ht는 time step t의 hidden variable이다. 확인해야 할 점은 이전의 Ht-1이 Whh와 곱해져 Ht에 전달된다는 점이다. 위의 hidden layer와 비교했을 때 가지는 분명한 차이점이다. Ht와 Ht-1의 관계는 이전 sequence의 역사적인 정보를 확인하고 유지하는 것이다. 여기서 computation이 recurrent 하기에 이번 챕터의 recurrent neural network라는 용어가 명명된 것이다.

 

 RNN에서의 output layer의 output을 보면 다음과 같다.

이 때문에 time step이 다르더라도 RNN은 항상 이런 model parameter를 사용하기에 step이 증가하여도 RNN의 parameterization cost는 증가하지 않는다.

 

 연산을 더욱 구체적으로 보자면 t의 hidden state는 input Xt와 이전의 hidden state와 concatenate 연산을 하고 이 concatenate 연산결과를 activation function과 함께 fully connected layer 연산을 한다.

 이 때 XtWxh + Ht-1Whh 연산은 matrix multiplication of concatenation of Xt & Ht-1과 concatenation of Wxh & Whh와 같다. 두 연산이 같은지 확인해보자.

X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
torch.matmul(X, W_xh) + torch.matmul(H, W_hh)
torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))

위 두 연산 모두 아래의 동일한 값을 가진다.

 

 이제 기본 연산을 알았으니 RNN for character-level language modeling에 대해 알아보자. 간단히 말하면 tokenizing을 하나의 단어 단위가 아닌 character 단위로 하는 것이다.

 위 사진이 이를 도식화한 것이다. 훈련 과정에서 output layer의 output에 softmax operation을 매 time step마다 수행하고 cross entropy loss를 사용해 model output과 label간의 cross entropy loss를 계산한다. O3는 이전 input m, a, c에 의해 결정되고 그 다음 character of sequence가 h이기에 loss of time step 3는 m, a, c와 label h를 기반으로 만들어진 다음 character의 확률 분포를 따르게 된다.

 

  그렇다면 language model quality는 어떻게 결정될까? It's raining 다음 단어를 예측하는 language model일 경우, 당연히 세개중 첫번째 문장을 만들 model이 able to predict with high accuracy token이라고 할 수 있다.

 그렇다면  language model의 quality는 computing the likelihood of sequence를 통해 확인할 수 있다고 생각할 수 있다. 하지만 이 숫자는 이해하고 비교하기 어렵다. 더 짧은 sequence를 가진 문장들이 긴 문장보다 likelihood of sequence를 더 크게 가지는 것은 당연하기 때문이다.

 여기서 information theory로 quality를 판단할 수 있는데 최근의 set of tokens로 다음의 token을 예상하는데 이 때 더 정확히 예측해야 한다. 따라서 정확한 예측이란 equence를 compressing 할 때 더 적은 bit를 사용하는 것이다. 이를 corss entroply loss averaged over all the n torkens of a sequence로 계산한다.

P는 language model에 의해 주어지고 Xt는 time step t에서 확인되는 token이다. 이것은 다른 길이의 문장에 대해서도 비교할 수 있게한다. 이 때 quantity를 perplexity라고 부른다.

 그 의미를 자세히 살펴보면 "harmonic mean of the number of real choices that we have when deciding which token to pick next"로 이해할 수 있다. 모델이 label token의 확률을 1로 계산하는 것이 최상의 경우이고 이 때 perplexity는 1이다. 또 모델이 label token의 확률을 0으로 계산하는 것이 최악의 경우이고 이 때 perplexity는 postive infinity이다.


5. RNN

위에서 만든 각각의 token은 train_iter에서 각각의 numerical index로 표현된다. 이 numerical index로 nerual network에 직접 훈련하지는 못한다. 각각의 token을 더 표현력이 있는 feature vector로 만들어야 하는데 이 때 사용하는 것이 one-hot encoding이다. 전체  vocabulary 길이 N이 있으면 token은 0부터 N-1까지의 인덱스를 가지고 길이 N의 0벡터를 만들어 해당 요소만의 poistion에 1의 값을 갖게 만드는 것이다.

F.one_hot(torch.tensor([0, 2]), len(vocab))

 매번 우리가 sample하는 minibatch의 모양은 (batch size, number of time steps)이다. one_hot은 이런 minibatch를 3차원의 텐서로 변환하고 마지막 차원은 vocabulary size와 같다. 이 input을 (number of time steps, batch size, vocabulary size) 모양의 output으로 변환한다. 이 모양으로 time step마다 minibatch의 hidden state를 업데이트한다. 다음과 같은 모양을 띈다.

X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape
X

 이제 training을 위한 과정을 진행해보자.

아래의 num_hidden은 조정가능한 하이퍼파라미터이고 language model을 training하는 것은 input과 output이 모두 같은vocabulary에 속하고 vocabulary size와 같은 차원을 가진다.

모델 파라미터 intializing은 다음과 같다.

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size
    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01
    # Hidden layer parameters
    W_xh = normal((num_inputs, num_hiddens))
    W_hh = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)
    # Output layer parameters
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # Attach gradients
    params = [W_xh, W_hh, b_h, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params

 다음과 같은 init_rnn_state 함수를 정의해 hidden state를 initialization한다.

 def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

RNN model은 input의 가장 바깥 차원을 loop하며 minibatch의 hidden state h를 업데이트한다. 여기서는 tahn function을 사용하였다.

def rnn(inputs, state, params):
    # Here `inputs` shape: (`num_steps`, `batch_size`, `vocab_size`)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # Shape of `X`: (`batch_size`, `vocab_size`)
    for X in inputs:
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)
class RNNModelScratch:
    """A RNN Model implemented from scratch."""
    def __init__(self, vocab_size, num_hiddens, device,
                 get_params, init_state, forward_fn):
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
        self.params = get_params(vocab_size, num_hiddens, device)
        self.init_state, self.forward_fn = init_state, forward_fn
    def __call__(self, X, state):
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
        return self.forward_fn(X, state, self.params)
    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state)
Y.shape, len(new_state), new_state[0].shape

 output의 모양을 보면 (number of time steps x batch size, vocabulary size)가지고 있고 hidden state 모양은 (batch size, number of hidden units)으로 유지된다.

 

 이제는 prediction function을 구현해보자. character를 정의하고 있는 user-provided prefix를 따르는 새로운 character를 만든다.

def predict_ch8(prefix, num_preds, net, vocab, device):
    """Generate new characters following the `prefix`."""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]:  # Warm-up period
        _, state = net(get_input(), state)
        outputs.append(vocab[y])
    for _ in range(num_preds):  # Predict `num_preds` steps
        y, state = net(get_input(), state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])

'Basic Deep Learning > Dive into Deep Learning 리뷰' 카테고리의 다른 글

[D2L] 9. Modern RNN  (0) 2022.08.08
[D2L] 7.1 Modern CNN  (0) 2022.07.26
[D2L] 6. CNN  (0) 2022.07.25
[D2L] 11.1 Optimization and Deep Learning  (0) 2022.07.13
[D2L] 4.8 Numerical Stability and Initialization  (0) 2022.07.11