카테고리 없음

(자연어처리 / word 기반 RNN) 한글 시 짓기 : 밑바닥 딥러닝2 버전...

미친토끼 2021. 7. 17. 09:26

<밑바닥에서 시작하는 딥러닝 2>의 6장과 7장에서 PTB (팬트리 뱅크) 데이터 셋을 이용해 문장 생성을 하는 예제가 있습니다.

이 부분을 공부하고 저도 한번 이 코드를 이용해 '영어로 된 영화 스토리' 데이터를 입력해 문장 생성을 해보려고 했으나 전처리가 제법 힘들었습니다. 그래서 포기했던 기억이 있네요.

 

PTB 데이터 셋의 train 부분만 해도 5.1M인데, 제가 작업 중인 한국어 시 데이터는 전부 2M 남짓해서(현재 3014 수), 전체로 따지면 PTB 데이터 규모의 1/3밖에 되지 않습니다. PTB는 희귀 단어를 <unk>(unknown)로 표시한 탓에 총 1만개의 어휘를 사용하고 있고, 저의 시 데이터들은 형태소 분석 종류에 따라 2만~2만2천 개 정도의 형태소 어휘를 사용합니다. <unk> 표시는 사용하지 않고 있어요.

 

문장 생성에서 희귀 단어를 <unk>로 표시하면 어휘가 적어져서 작업하기에는 좋지만, 휘귀 단어들이 올 자리가 비어버리기 때문에 자연스런 문장이 안 된다고 생각합니다. 가령,

 

'툇마루에 앉아 하늘을' 이란 문장에서 '툇마루'를 '<unk>'로 처리하면 '<unk>에 앉아 하늘을'이 되지요. 원래의 <unk>가 뭐였는지 알 수 없고, <unk>가 뭐였을지를 상상하게 하는 점은 있겠네요. <unk>를 skipwords로 정의해버리면 아예 문장이 달라지겠지요. '에 앉아 하늘을'이 되어 버려 어디에 앉아 있는지 알 수 없을 뿐더러, 바로 앞에 주어와 '에'가 붙어버리면 이상한 문장이 되어버리겠지요.

 

'$'는 제 데이터에는 포함되어 있지 않습니다. PTB 코드에서는 '\n'을 <eos>(end of sentence) 로 대체해서 예측 작업을 하는데, <eos>도 하나의 단어로 취급되므로, '\n'을 '<eos>'로 replace할 때, ' <eos> '로 (좌우로 공백 하나씩 삽입)해야 공백 기준 어휘 분리(split())할 때 <eos>가 제대로 어휘로 분리됩니다.

 

그외 전처리는 PTB 데이터셋 전처리를 미리 참고 해서 염두에 두고 시 데이터 전처리를 했던 지라, 무던하게 적용할 수 있을 것 같습니다.

 

- 시 데이터에는 문장부호가 일체 없으며

- 숫자는 한글 표기, 혹은 'N'으로 표기되어 있으며

- 문장의 끝은 '\n'으로 되어 있고

- 한줄의 문장은 한 편의 시에 해당하거나 (절 표기가 있는 시의 경우) 한절의 시에 해당합니다.

- 영어 표기, 한자 표기는 모두 삭제되었으며

- 띄어쓰기 기준 어휘 분리를 하지 않고 형태소 분석(mecab, khaiii) 기준으로 토큰을 만들어 토큰 사이를 모두 띄워놓았습니다.

 

<밑바닥부터 시작하는 딥러닝 2> 소스 코드의 dataset 폴더에 보면 ptb.py라는 프로그램이 있습니다. PTB 데이터 셋을 읽어 훈련용 시퀀스를 만들어주는 코드입니다. 

PTB 데이터는 훈련용, 테스트용, 검증용 데이터 총 3개로 나누어져 있습니다.

그래서 저도 시 데이터를 8:1:1의 비율로 대략 나누어서 그 명칭을 각기 ptb.train.txt, ptb.test.txt, ptb.valid.txt로 명명하고, 이 파일들을 dataset 폴더 안에 넣어놓았습니다. 폴더 안에 이미 존재하는 동명의 실제 PTB 파일들은 별도로 백업하여 놓았고요.

이렇게 해서 ptb.py를 실행하면, 오류가 발생합니다. 이 부분에서 발생합니다.

 

 corpus = np.array([word_to_id[w] for w in words])

 

ptb.py는 ptb.train.txt에 있는 어휘로 어휘 사전을 만드는데, 이후 ptb.test.txt나 ptb.valid.txt를 입력해도 그곳에 있는 어휘들이 모두 기존 ptb.train.txt 기반 어휘 사전에 존재해야 하기 때문입니다.

그래서 저는 시 데이터가 들어 있는 ptb.train.txt를 원래 시 데이터의 0.8 분할된 것이 아닌 전체 데이터를 ptb.train.txt로 다시 만들어 넣었습니다. 이렇게 하면, test용 시 데이터, 검증용 시 데이터의 어휘들도 모두 train용 데이터 기반 어휘 사전에 포함되기 때문에 오류가 사라집니다.

 

두 번째 오류는, ptb.py의 다음 코드에서 발생합니다.

 

 words = open(file_path).read().replace('\n', '<eos>').strip().split()

 

여기서 '\n'을 '<eos>'로 바꾸게 되는데, '\n' 앞뒤로 공백이 존재하지 않을 경우, '\n'을 그냥 '<eos>'로 바꾸면 '바보야\n나는'이라는 문장이 '바보야<eos>나는'으로 바뀌어 되어, 공백 기준으로 토큰들을 자를 때 이게 하나의 단어가 되어버려서 어휘 사전에 없게 되는데, 그래서 또다시 어휘사전에 존재하지 않는다고 

corpus = np.array([word_to_id[w] for w in words])

 

이 코드에서 오류 메시지를 냅니다. 해결책은, '<eos>'를 ' <eos> '로 바꾸면 됩니다. 동일 코드가 ptb.py에 한 곳 더 있는데 그곳도 ' <eos> '로 바꾸면 됩니다.

 

dataset 폴더에 기존의 .pkl이나 .npy파일과 뒤섞여 로딩 오류가 발생할 수 있으니, 자주 지우면서 작업했습니다.

ptb.py를 실행하는데 이상이 없으면 ch06/ 폴더에서 훈련용 코드를 실행시킵니다.

코드 입장에서는 한글이든 영어든 유니코드이기 때문에 한글 시 데이터를 훈련시키는 것이나 영문 문장을 훈련시키는 것이나 동일합니다. 달라진 포맷이 있으면 그것만 맞춰주면 됩니다.

 

train_rnnlm.py 에서 저는 batch_size = 128, max_epoch = 1로 해서 실행을 시켜보았습니다.

문제 없이 잘 실행됩니다.

 

*** 제일 중요한 것을 언급하지 않았는데, 제이슨 씨의 word 기반 RNN 코드는 시퀀스 1개의 길이가 51로 기본 되어 있어, 50개 단어를 학습하고 다음에 오는 1개의 단어를 예측하는 형태로 진행됩니다. 그래서 문장 생성할 때도 그쪽 어휘 사전에 등록되어 있는 단어들을 start 문장으로 제공해야 하며, 제이슨 씨 코드에서는 랜덤으로 시퀀스 한 개를 뽑아 그것을 start 문장으로 제공하고 다음에 오는 단어를 예측하는 식입니다. 그 다음에는 start 문장의 두 번째 문자부터 새로 영입된 문자 1개를 start 문장으로 다시 공급하고, 이런 식으로 예측 희망 문자에 달할 때까지 지속됩니다.

 

*** 이에 반해 <밑바닥에서 시작하는 딥러닝 2>의 작가 사이토 고키 씨의 코드는, 단어를 하나 주고 그 다음 단어는 이런 단어다, 라고 훈련 데이터로 학습시켜서, 문장 생성 시에, start 단어를 주면 그 다음 단어를 확률로 예측하는 방식입니다. 비결정론적 확률 예측이라는 점에서는 양자가 같으나, start 단어만 줄지, 아니면 하나의 단락(시퀀스) 전체를 start 시에 줄지 하는 차이인데 이게 문장 생성 시에 어떤 영향을 미치는지 그 차이는 좀 관찰해봐야 할 것 같습니다.

 

문장 생성 코드는 7장의 generate_text.py 입니다.

 

start_word = 'you'
start_id = word_to_id[start_word]
skip_words = ['N', '<unk>', '$']

 

start_word를 한글로 바꾸어줍니다. 물론 word_to_id 사전에 있어야 합니다. 시에 있을 법한 단어를 적어주면 어지간하면 2만 여개의 시 형태소에 포함될 겁니다.

'<unk>'와 '$'은 저의 데이터에 아예 없고, 'N'은 스킵하지 않으려고 합니다. 그래서 이 줄을 주석 처리하려니, skip_words라는 변수를 밑에서 계속 사용하네요. 남겨둘 수밖에 없는데, 어휘 사전에 있는 아무 말이나 하나 적어놓죠.

 

start_word = '그대'
start_id = word_to_id[start_word]
skip_words = ['십이월']

 

'그대'와 '십이월'이 단어 사전에 있는지 없는지는, ipython이나 python 명령행, 주피터 노트북에서 word_to_id['해당단어'] 를 찍어보면 됩니다.

 

이제 이 코드를 실행하면 새로운 오류가 등장합니다.

 

Traceback (most recent call last):
  File "generate_text.py", line 13, in <module>
    model.load_params('../ch06/Rnnlm.pkl')
  File "/home/don/deep2/ch07/../common/base_model.py", line 49, in load_params
    param[...] = params[i]
ValueError: could not broadcast input array from shape (22762,100) into shape (10000,100)
----

훈련 코드에서, 학습한 파라미터값을 파일로 저장하는데, 문장 생성 코드에서 이것을 불러오려니 형상이 안 맞다는 이야기지요.

코드들을 뒤져보고 원인을 발견했습니다.

훈련코드에서 Rnnlm 클래스를 호출할 때는 이렇게 합니다.

 

model = Rnnlm(vocab_size, wordvec_size, hidden_size)

 

이때 vocab_size 는 22762입니다. 

문장 생성 코드에서는 훈련코드에서 학습된 가중치를 적용하기 위해, 모델을 새로 만들게 되는데 그때 이렇게 생성하고 있네요.

 

model = RnnlmGen()

 

RnnlmGen() 클래스를 호출하고 있는데, 이때 rnnlm() 클래스를 상속 받습니다.

그러면서 vocab_size를 명시하지 않아 기본값들이 적용됩니다. 이렇게요.

 

class Rnnlm(BaseModel): 

      def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):

            .....

 

vocab_size(어휘 갯수)가 10,000개로 적용되는데, 이것은 PTB 데이터 어휘 개수지요. 

결국 PTB 어휘 개수를 하드 코딩해둔 것인데...

이것을 22762로 바꾸면 되겠네요.

그러고 나서 문장 생성 코드를 다시 돌려봅니다.

이 코드에서 처음으로 생성된 한글 문장입니다.

 

그대 맹금 들 원해요 석등 말 과 뻣뻣 을 소스라쳐 내려진 아 내 을 만취 오늘 길 들이쉬 잉잉대 설치 산다는 는지 자객 별난 각 동반자 도 물수건 눈빛 던 꽃씨 을 이즈음 자스민 차려진 는 나 내 도 너 았 다 혼란 로 홍대 을 허물어져 하 도 의 해도 안기 나 을 쾅 그 다든지 사건 오 매만져 들으러 날 다고 지만 됩시다 는 장작불 웃음 했었을 하 지 견우 에 손 솟 한 오동나무 없 을 탁발승 도 이름 다 떄문 분주히 을 고분고분 지 내동면 꽃 일 심지 겠 없 새벽 길녁.

 

1 에폭 훈련에, 기본 rnnlm 모델이라 별 의미는 없습니다.

 

* 6장의 훈련코드를 실행할 때 쿠파이(cupy)를 이용할 수 있는데, 그 경우에 발생하는 오류의 해결법을 별도로 적어놓았으니 본 사이트에서 검색해보세요.

 

* 이제 better_rnnlm 모델로 훈련을 많이 시키고, khaiii 나 mecab으로 형태소 분석한 것을 적용해봐야겠습니다.

(하, 이 지점에서 RTX 3060 사러 나가봐야 하나.... CPU로 훈련시키려면 시간 많이 걸리는데...--; )