딥러닝

밑바닥부터 시작하는 딥러닝 - RNN을 사용한 문장 생성

Leesemo 2021. 11. 3. 21:44

지금까지 RNN과 LSTM의 구조와 구현을 살펴봤다. 

이번 글에서는 언어 모델을 사용해 '문장 생성'을 수행해볼 것이다. 말뭉치를 사용해 학습한 언어 모델을 이용하여 새로운 문장을 만드는 것이다. 그런 다음 개선된 언어 모델을 이용하여 더 자연스러운 문장을 생성하는 것을 해볼 것이다.


RNN을 사용한 문장 생성의 순서

언어 모델은 지금까지 주어진 단어들에서 다음에 출현하는 단어의 확률분포를 출력한다. 위의 그림은 'I'라는 단어를 주었을 때 출력한 확률분포이다. 이 결과를 기초로 다음 단어를 새로 생성하려면 어떻게 해야 할까?

첫 번째로 확률이 가장 높은 단어를 선택하는 방법이다. 확률이 가장 높은 단어를 선택할 뿐이므로 결과가 일정하게 정해진다. 또한 '확률적'으로 선택하는 방법이다. 각 후보 단어의 확률에 맞게 선택하는 것으로, 확률이 높은 단어는 선택되기 쉽고, 확률이 낮은 단어는 선택되기 어려워진다.

위의 그림은 확률분포로부터 샘플링을 수행한 결과로 'say'가 선택된 경우이다. 실제로 확률분포에서는 'say'의 확률이 가장 높기 때문에 'say'가 샘플링될 확률이 가장 높기도 하다. 다른 단어들도 해당 단어의 출현 확률에 따라 정해진 비율만큼 샘플링될 가능성이 있다는 점도 주의해야 한다.

 

그러면 계속해서 두 번째 단어를 샘플링해보자. 방금 생성한 단어인 'say'를 언어 모델에 입력하여 다음 단어의 확률분포를 얻는다. 그런 다음 그 확률분포를 기초로 다음에 출현할 단어를 샘플링하는 것이다.

다음으로 이 작업을 원하는 만큼 반복한다. 그러면 새로운 문장을 생성할 수 있다.

여기에서 주목할 점은 이렇게 생성한 문장은 훈련 데이터에는 존재하지 않는, 말 그대로 새로 생성된 문장이다. 언어 모델은 훈련 데이터를 암기한 것이 아니라, 훈련 데이터에서 사용된 단어의 정렬 패턴을 학습한 것이기 때문이다.


문장 생성 구현

그럼 문장을 생성하는 코드를 구현해보자.

class RnnlmGen(Rnnlm):
    def generate(self, start_id, skip_ids = None, sample_size = 100):
        word_ids = [start_id]
        
        x = start_id
        while len(word_ids) < sample_size:
            x = np.array(x).reshape(1, 1)
            score = self.predict(x)
            p = softmax(score.flatten())
            
            sampled = np.random.choice(len(p), size = 1, p = p)
            if (skip_ids is None) or (sampled not in skip_ids):
                x = sampled
                word_ids.append(int(x))
                
        return word_ids

이 클래스에서 문장 생성을 수행하는 메서드는 generate()이다. 인수 중 start_id는 최초로 주는 단어의 ID, sample_size는 샘플링하는 단어의 수를 말한다. 그리고 skip_ids는 단어 ID의 리스트인데, 이 리스트에 속하는 단어 ID는 샘플링되지 않도록 해준다.

generate() 메서드는 가장 먼저 model.predict(x)를 호출해 각 단어의 점수를 출력한다. 그리고 p = softmax(score) 코드에서는 이 점수들을 소프트맥스 함수를 이용해 정규화한다. 이것으로 확률분포 p를 얻을 수 있다. 그런 다음 확률분포 p로부터 다음 단어를 샘플링한다.

그렇다면 RnnlmGen 클래스를 사용해 문장을 생성해보자.

from dataset import ptb

corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
corpus_size = len(corpus)

model = RnnlmGen()

# 시작(start) 문자와 건너뜀(skip) 문자 설정
start_word = 'you'
start_id = word_to_id[start_word]
skip_words = ['N', '<unk>', '$']
skip_ids = [word_to_id[w] for w in skip_words]

# 문장 생성
word_ids = model.generate(start_id, skip_ids)
txt = ' '.join([id_to_word[i] for i in word_ids])
txt = txt.replace(' <eos> ', '.\n')
print(txt)

여기에서는 첫 단어를 'you'로 하고, 그 단어 ID를 start_id로 설정한 다음 문장을 생성한다. 샘플링하지 않을 단어로는 ['N', '<unk>', '$']를 지정했다. 문장을 생성하는 generate() 메서드는 단어 ID들을 배열 형태로 반환한다. 그래서 그 단어 ID 배열을 문장으로 변환해야 하는데, txt = ' '.join([id_to_word[i] for i in word_ids]) 코드가 그 일을 담당한다.

join() 메서드는 [구분자].join 형태로 작성하여, 리스트의 단어들 사이에 구분자를 삽입해 모두를 연결한다.

출력 결과는 위와 같다. 단어들을 엉터리로 나열한 글이 출력되었다. 모델의 가중치 초깃값으로 무작한 값을 사용했기 때문에 의미가 통하지 않는 문장이 출력된 것이다. 그렇다면 학습을 수행한 언어 모델은 어떻게 다른지 살펴보자.

위와 같은 문장이 출력되었다. 문법적으로 이상하거나 의미가 통하지 않는 문장이 섞여 있지만, 그럴듯한 문장도 존재한다. 어느 정도 올바른 문장이라고 할 수 있겠지만, 부자연스러운 문장도 발견되기 때문에 개선이 필요하다.