나무위키 확률론적 언어모형 구현실습으로 이해하는 RNN

2019-05-12

Data_Engineering_TIL_(20190511)

study program : https://www.fastcampus.co.kr/extension_des

[학습목표]

1) konlpy 형태소 분석기 이해

2) 확률적 언어모형 기초개념 이해

3) RNN(Recurrent Neural Networks) 기초개념 이해

4) Tensorflow 개요 이해

5) 나무위키 텍스트 데이터를 이용한 RNN 모델 구현원리 이해

[학습기록]

1. konlpy 형태소 분석기

형태소 분석을하는건데 단어 하나하나를 다 뜯어서 명서면 명사, 동사면 동사 이런식으로 테깅을 할 수 있다.

동음이의어 명사가 두개가 있으면 이를 구분하는 건 어렵지만 동사, 명사로 나뉘어지는 동음이의어는 구분할 수 있다.

형태소 분석기는 시중에 여러종류가 있는데 대부분 유료이고 우리는 트위터에서 오픈소스로 개발한 형태소 분석기를 쓸 것이다.

from konlpy.tag import Twitter

result = Twitter().pos("나무위키 말뭉치를 만들어보자")
# 어떤 문장을 넣으면 for문의 출력내용처럼 단어를 하나씩 뜯어서 형태소가 뭔지 tagging해준다.

# ' ' (스페이스) 단위로 문장내 단어를 뜯어서 구분한다.
for pos in result:
    print(pos[0] + ' ' + pos[1])
    
tagger = Twitter()
나무 Noun
위키 Noun
말뭉치 Noun
를 Josa
만들어 Verb
보자 Verb
# 5번 결과처럼 '단어/형태소' 이런식으로 만들려고 함수를 정의

def flat(content):
    return ["{}/{}".format(word, tag) for word, tag in tagger.pos(content)]
tagged = flat("나무위키 말뭉치를 만들어보자")
' '.join(tagged)
'나무/Noun 위키/Noun 말뭉치/Noun 를/Josa 만들어/Verb 보자/Verb'
input_filename = 'text_exam.txt'
output_filename = 'text_exam2.txt'
with open(output_filename, 'w', encoding='utf-8') as output_file:
    for line in open(input_filename, 'r', encoding='utf-8'):
        for sentence in line.split('.'):
            tagged = flat(sentence)
            if len(tagged) > 1:
                a_line = ' '.join(tagged)
                output_file.write(a_line + '\n')
  • text_exam.txt -> text_exam2.txt 변환결과는 아래 그림과 같다.

1

2. 확률적 언어모형

2

위의 식에서 P(o\c)는 확률적 언어모델을 의미한다.

C는 centerword라고해서 가운데 단어를 말하고, O는 outerword다.

그래서 주변단어가 있을때 가운데 단어를 예측하는 것도 있고, 가운데 단어가 있을때 주변단어를 예측하는 것도 있다.

위의 식의 경우는 가운데 단어가 있을때 주변단어를 예측하는 케이스이다.

위에 식에서 uo가 outerword를 의미하고 vc가 가운데 단어에 대한 벡터를 의미한다. 전체단어 갯수가 W 니까 exp(uwvc)는 이게 백터간의 내적이기 때문에 벡터의 방향이 얼마만큼 일치하는가가 나오게 된다. 그래서 전체단어에 대해서 가운데 단어의 해당하는 outerword에 대해 얼마나 겹치는지를 말하는 것이다.

그래서 이런식으로 언어모델을 정의해서 이 언어모델이 최대화 되도록 u,v를 학습시키게 된다.

이것도 역시 코스트의 그레디언트를 구해서 학습시키는 방식이다.

linear function과 다르게 cost function이 복잡해진다.

  • 확률적 언어모형에서 negative sampling

이러한 방법으로 확률적 언어모형을 만들게 되면 중심단어 하나에 대한 모든 단어의 확률을 계산해야 하기 때문에 계산양이 많아지게 된다. 예를들어서 단어가 10만개짜리의 코퍼스가 있고 우리가 그걸 가지고 확률적 언어모델을 만든다고 했을때 10만개 중에 확률을 계산해야해서 엄청나게 계산양이 많아지게 된다.

그래서 계산양을 줄이고자 매 스텝마다 모든 단어를 전부 계산해주는 것이 아니라 전체 단어에서 5 ~ 20개의 단어만 랜덤으로 뽑아서 이 랜덤으로 뽑힌 단어들이 등장할 확률을 계산하는 것이다. 그것을 negative sampling이라고 하는데 당연히 전체를 했을때 보다는 정보량이 줄어들겠지만 이 방식을 써도 전체단어를 썼을때와 유사한 효과를 얻을 수 있고 계산양은 확 줄일 수 있다.

negative sampling은 stocastic gradient decent의 아이디어와 비슷하다.

  • seq2vec, doc2vec

word를 벡터공간에 임베딩 한것처럼, word의 집합인 sequence 그리고 더 나아가 document도 벡터공간에 임베딩 할 수 있다. word2vec 또한 문장, 문서의 의미가 비슷하다면 시슷한 곳에 위치하고 차원들이 의미를 가지게 될 것이다.

비슷한 벡터 방향을 갖는 것을 찾는다면 여기에서 의미를 도출해낼 수 있다.

실무에서는 예를들어 쇼핑몰을 운영한다고 하면 item2vec을 만들 수 있을것이다. 상품이름이나 상품 설명을 item2vec으로 만든다면 카테고리를 구성하는데 도움을 줄 수 있거나 심지어는 카테고리를 대신할 수도 있을 것이다. 또한 아이템 추천에 활용도 가능할 것이다.

3. RNN(Recurrent Neural Networks)

  • 확률적 언어모델

우리가 언어모형을 만들게 되면 얘가 문장의 순서, 단어선택 등을 예측할 수 있다. 그래서 번역을하거나 심지어는 소설을 쓸수도 있다.

1) 연속된 단어들의 등장확률을 예측하는 모델

2) ex)

배가고파서 밥을 xxx

xxx에 들어갈 단어는?

p(먹었다) = 0.25, p(먹는다) = 0.15, p(지었다) = 0.05 , p(자동차) = 0.000001

4) 문장의 순서나 단어선택 등을 예측할 수 있어서 자동번역 등 많은 NLP Task에 유용하다.

5) 확률적 언어모델을 만드려면 이전에 나오는 단어를 많이 참조할수록 정확해진다.

주변단어를 몇개를 볼 것이냐에 따라 달라진다.

주변에 두개를 보겠다고 하면 2-gram이고, 3개를 보겠다면 3-gram, … , n개를 보겠다면 n-gram이 되는것이다.

ex) 2-gram, 3-gram, 4-gram, .. n-gram

당연하게도 넓은 범위의 단어를 볼 수록 좋은 문장을 만들어 낼수 밖에 없다.

6) 그런데 n-gram을 늘릴수록 메모리가 엄청나게 필요하다.

예를들어서 소설을 만든다고 하면 소설의 첫페이지부터 마지막 페이지까지 이어져야 하는데 그러려면 소설책의 마지막 단어를 쓰기까지 그 앞에 있는 모든 문맥들을 이해할 수 있어야 하기 때문에 만약에 10만자라고 하면 10만-gram의 모델을 만들게 되는 것이다. 현실적으로 이렇게는 할 수 없고

이런 문제를 해결해 줄 수 있는 것이 RNN이다.

  • RNN(Recurrent Neural Networks)

RNN은 아래 그림과 같이 데이러를 input 넣어서 나온 output을 다시 input으로 넣어주는 Recurrent한 것이 특징이다.

인터넷 커뮤니티에서 댓글 이어달기와 비슷한 아이디어다.

위에서 소설책 얘기나오면서 10만-gram을 언급했었는데 현실적으로는 10만-gram이라는 것은 불가능하니까 나온게 RNN이다.

RNN에서는 앞에 나온 문맥이라는 것을 state라는 상태에 추상화된 형태로 가지고 있고, 앞의 결과 플러스 그 다음상태를 업데이트한 상태값을 갖고 그래서 나온 결과값을 그 다음 상태를 예측하고 또 그 스텝의 추상화된 상태를 업데이트해주는 이런 방식으로 네트워크로 이루어져있다.

그래서 앞에 있는 단어를 무작정 참조하는 것이 아니고 추상화된 상태값과 같이 만드는 것이다. 그래서 상태값을 어떻게 잘 만들면 저 앞에 단어를 전부다 본거 같은 효과를 주는 것이다.

아래 그림에서 각각의 노드는 입력을 받아서 출력을 뱉는 것인데 중간에 s라는데 들어간다. s는 추상화된 상태값을 입력 데이터에 반영해주는 것이다.

s는 sequence(순서)라고 생각하면 된다. 그래서 이 s를 통해 이전 sequence 부터 쭈욱 방대한 정보가 녹아들어가 있다.

그래서 이렇게 나온 출력값을 Unfold라고해서 단어가 처음부터 끝까지 연속해서 들어옴에 따라서 state도 계속 바뀌고, output도 바뀌게 된다.

3

위의 그림에서 “배가 고파서 나는 밥을 xxx”은 xxx을 예측하기 위해 앞에 4개의 단어를 참조한 것이고, 이는 RNN에서 4개의 계층의 neural network가 작동한 것과 같다.

  • RNN을 학습시키는 것은 많은 컴퓨팅 파워와 데이터양이 필요하다.

  • vanishing gradient 문제도 발생하는 경우가 있다.

RNN 모델의 네트워크가 복잡하기 때문에 발생하는 문제다.

학습 시 그레디언트를 갖고 미분값을 가지고 weight값을 업데이트 하는 원리는 같은데 너무 오래전의 정보까지 참조하려다보니, 어디서 잘못했었는지 학습하기가 어려운 증상이다.

  • exploding gradient 문제도 있다. 그래서 이런 문제들을 ReLU나 Clipping 등 여러가지 테크닉으로 극복 할 수 있다.

4. Tensorflow

  • 구글이 만든 오픈소소화 된 머신러닝 라이브러리

  • Tensor들의 계산 그래프를 정의해놓고, 데이터를 흐르게하여(Flow) 머신러닝 알고리즘을 구현

  • Tensor : 벡터의 확장개념으로 다차원의 array data

  • Python API를 제공하고, CPU와 GPU를 활용한 연산이 가능하다

  • 텐서플로우에는 Low level API와 High level API(estimator라고 불리우는)가 존재한다.

  • 최근에 2.0베타 버전이 나오면서 상당히 쉬워졌다.

  • Computational Graph

1) Computational Graph를 만들고 실행한다.

2) 디버깅하기 힘들다.

3) graph의 노드는 tensor들을 받아서 다른 tensor로 내보낸다.

4) constant는 가장 간단한 형태의 노드중 하나로 입력은 없고 단일 output을 내보낸다.

6) 1.xx 버전시절 사용예시 및 기본개념

다시말해서 2.xx에서는 안쓰는 개념이다.

2.xx 에서는 더 직관적으로 업그레이드 되었다.

4

7) Tensorflow Train API

5

위에 linear regression 예시에서 데이터가 들어오는 것을 플레이스 홀더라고 한다. 학습을 해야하는것은 variable이다.

8) Tensorflow Estimator API

위의 예시에서 좀더 하이레벨의 코딩이다.

6

9) Tensorflow 2.0 (Alpha)

7

  • GPU를 사용하려면 tensorflow-gpu 버전을 설치해야한다.

또한 CUDA toolkit, CuDNN 라이브러리도 추가로 셋업이 필요하다

텐서플로우 버전, 쿠다, cudnn 버전을 아주 정확하게 맞추지 않으면 실행되지 않는 경우가 많으니 주의해야 한다.

5. 나무위키 텍스트 데이터를 이용한 RNN 모델 구현

나무위키 데이터 : 30메가짜리 텍스트 데이터

통상 단어 단위로 RNN을 하는 경우가 많은데 이번에는 더 쪼개서 글자 단위로 뜯어서 RNN을 구현해보려고 한다.

ex) 나무위키 텍스트 -> ㄴ ㅏ ㅁ ㅜ ㅇ ㅜ ㅣ ㅋ ㅣ ㅌ ㅔ ㄱ ㅅ ㅡ ㅌ ㅡ

먼저 txt 데이터를 글자를 사전으로 만든다

import numpy as np
import tensorflow as tf

input_filename = 'namuwiki_20180326_charactor_mini_2.txt'
input_file = open(input_filename, 'r', encoding='utf8')
data = input_file.read()
import collections
counter = collections.Counter(data)
counter.items()
dict_items([(' ', 918211), ('ㅌ', 65575), ('ㅡ', 314996), ('ㄹ', 448618), ('ᴥ', 2723452), (':', 22377), ('ㄷ', 255021), ('ㅏ', 553745), ('ㄴ', 538699), ('ㄸ', 12062), ('ㅅ', 272555), ('1', 42663), (',', 31509), ('o', 9916), ('t', 9160), ('h', 5505), ('e', 12954), ('r', 6658), ('=', 36062), ('ㅎ', 200178), ('ㅇ', 862303), ('ㅓ', 268169), ('d', 3403), ('ㅂ', 160850), ('ㅔ', 136146), ('ㄱ', 463540), ('ㅜ', 197541), (')', 11108), ('\n', 100000), ('*', 39553), ('ㅟ', 17940), ('ㅁ', 206254), ('ㅗ', 266779), ('ㅕ', 147901), ('ㅛ', 21475), ('|', 1926), ('<', 2862), ('a', 8403), ('b', 1692), ('l', 4704), ('i', 6492), ('g', 2589), ('n', 7128), ('c', 3319), ('>', 5708), ('#', 1028), ('F', 2387), ('(', 10764), ('R', 1050), ('ㅈ', 232121), ('ㅣ', 432343), ('ㅖ', 12170), ('ㅐ', 134809), ('ㅘ', 48539), ('?', 1655), ('ㅚ', 40460), ('!', 2069), ('4', 11143), ('0', 22326), ('2', 29575), ('ㅋ', 32754), ('8', 9215), ('m', 3423), ('5', 10255), ('k', 1428), ('ㅊ', 73564), ('ㅢ', 53735), ('ㅆ', 72444), ('ㅄ', 4490), ('ㄲ', 12964), ('ㅠ', 18925), ('ㅍ', 50218), ('ㅝ', 39827), ('ㅑ', 14984), ('~', 1117), ("'", 13828), ('.', 9798), ('ㅙ', 1706), ('3', 13870), ('ㅉ', 4157), ('ㅃ', 3096), ('ㄶ', 5422), ('ㅞ', 1054), ('6', 9246), ('ㅀ', 533), ('"', 3853), ('ㄼ', 240), ('ㄻ', 361), ('ㄺ', 1080), ('A', 3628), ('S', 4238), ('K', 2141), ('ㅒ', 208), ('7', 7802), ('/', 2521), ('[', 1252), (']', 982), ('ㄵ', 152), ('-', 14917), ('9', 10732), ('p', 1752), ('s', 5953), ('u', 3367), ('w', 1763), ('y', 2515), ('V', 1224), ('O', 1850), ('C', 3246), ('L', 1803), ('I', 2710), ('D', 2495), ('v', 1987), ('j', 191), ('U', 729), ('P', 1846), ('T', 3160), ('H', 942), ('N', 1546), ('E', 1806), ('M', 2596), ('B', 3095), ('G', 1657), ('&', 259), ('Q', 98), ('_', 3394), ('f', 2121), ('ㄳ', 98), ('x', 311), ('J', 566), ('X', 529), ('W', 1060), ('Z', 183), ('%', 1087), ('q', 74), ('Y', 529), ('+', 380), ('ä', 1), ('ø', 1), ('z', 166), ('·', 448), ('\xa0', 8), ('ㄿ', 5), ('×', 8), ('\t', 7), (';', 50), ('{', 90), ('}', 81), ('^', 49), ('é', 18), ('ã', 1), ('ç', 2), ('«', 1), ('»', 1), ('\\', 2), ('ö', 4), ('°', 2), ('ㄽ', 1), ('$', 3), ('ㄾ', 11), ('±', 5), ('ü', 6), ('ß', 2), ('Ø', 1), ('É', 8), ('è', 1), ('ì', 2), ('à', 2), ('@', 3), ('²', 4), ('`', 3), ('Á', 2), ('á', 1), ('î', 1), ('â', 1), ('À', 1), ('Ó', 10), ('Ê', 1), ('Ú', 1), ('¡', 5), ('Í', 2), ('ú', 2)])
counter_item_sorted = sorted(counter.items(), key=lambda x: x[0])
chars = list(map(lambda x: x[0], counter_item_sorted))
print(chars)
['\t', '\n', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', '\xa0', '¡', '«', '°', '±', '²', '·', '»', 'À', 'Á', 'É', 'Ê', 'Í', 'Ó', '×', 'Ø', 'Ú', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'ç', 'è', 'é', 'ì', 'î', 'ö', 'ø', 'ú', 'ü', 'ᴥ', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ']
# 아래 결과에 각각의 번호는 의미가 없고 그냥 ㅅ은 150번, ㄹ은 138번이라고 라벨링 되어 있는것이다.
# 왜냐하면 컴퓨터는 문자를 인식할 수 없어서 숫자형식으로 만들어서 처리해야하기 때문이다.
vocab_size = len(chars)
vocab = dict(zip(chars, range(vocab_size)))
print(vocab)
{'\t': 0, '\n': 1, ' ': 2, '!': 3, '"': 4, '#': 5, '$': 6, '%': 7, '&': 8, "'": 9, '(': 10, ')': 11, '*': 12, '+': 13, ',': 14, '-': 15, '.': 16, '/': 17, '0': 18, '1': 19, '2': 20, '3': 21, '4': 22, '5': 23, '6': 24, '7': 25, '8': 26, '9': 27, ':': 28, ';': 29, '<': 30, '=': 31, '>': 32, '?': 33, '@': 34, 'A': 35, 'B': 36, 'C': 37, 'D': 38, 'E': 39, 'F': 40, 'G': 41, 'H': 42, 'I': 43, 'J': 44, 'K': 45, 'L': 46, 'M': 47, 'N': 48, 'O': 49, 'P': 50, 'Q': 51, 'R': 52, 'S': 53, 'T': 54, 'U': 55, 'V': 56, 'W': 57, 'X': 58, 'Y': 59, 'Z': 60, '[': 61, '\\': 62, ']': 63, '^': 64, '_': 65, '`': 66, 'a': 67, 'b': 68, 'c': 69, 'd': 70, 'e': 71, 'f': 72, 'g': 73, 'h': 74, 'i': 75, 'j': 76, 'k': 77, 'l': 78, 'm': 79, 'n': 80, 'o': 81, 'p': 82, 'q': 83, 'r': 84, 's': 85, 't': 86, 'u': 87, 'v': 88, 'w': 89, 'x': 90, 'y': 91, 'z': 92, '{': 93, '|': 94, '}': 95, '~': 96, '\xa0': 97, '¡': 98, '«': 99, '°': 100, '±': 101, '²': 102, '·': 103, '»': 104, 'À': 105, 'Á': 106, 'É': 107, 'Ê': 108, 'Í': 109, 'Ó': 110, '×': 111, 'Ø': 112, 'Ú': 113, 'ß': 114, 'à': 115, 'á': 116, 'â': 117, 'ã': 118, 'ä': 119, 'ç': 120, 'è': 121, 'é': 122, 'ì': 123, 'î': 124, 'ö': 125, 'ø': 126, 'ú': 127, 'ü': 128, 'ᴥ': 129, 'ㄱ': 130, 'ㄲ': 131, 'ㄳ': 132, 'ㄴ': 133, 'ㄵ': 134, 'ㄶ': 135, 'ㄷ': 136, 'ㄸ': 137, 'ㄹ': 138, 'ㄺ': 139, 'ㄻ': 140, 'ㄼ': 141, 'ㄽ': 142, 'ㄾ': 143, 'ㄿ': 144, 'ㅀ': 145, 'ㅁ': 146, 'ㅂ': 147, 'ㅃ': 148, 'ㅄ': 149, 'ㅅ': 150, 'ㅆ': 151, 'ㅇ': 152, 'ㅈ': 153, 'ㅉ': 154, 'ㅊ': 155, 'ㅋ': 156, 'ㅌ': 157, 'ㅍ': 158, 'ㅎ': 159, 'ㅏ': 160, 'ㅐ': 161, 'ㅑ': 162, 'ㅒ': 163, 'ㅓ': 164, 'ㅔ': 165, 'ㅕ': 166, 'ㅖ': 167, 'ㅗ': 168, 'ㅘ': 169, 'ㅙ': 170, 'ㅚ': 171, 'ㅛ': 172, 'ㅜ': 173, 'ㅝ': 174, 'ㅞ': 175, 'ㅟ': 176, 'ㅠ': 177, 'ㅡ': 178, 'ㅢ': 179, 'ㅣ': 180}
tensor = np.array(list(map(vocab.get, data)))
print(data[0:20])
print(tensor[0:20])
 ㅌㅡㄹᴥ:ㄷㅏᴥㄹㅡㄴᴥ ㄸㅡㅅᴥ1,
[  2 157 178 138 129  28 136 160 129 138 178 133 129   2 137 178 150 129
  19  14]
tensor.shape
(10986246,)
# 위에 코드처럼 사전을 만든 다음에 batch로 학습을 시키기 위해서 데이터를 아래와 같이 가공한다
# 배치 하나에 돌릴 데이터 갯수랑, 데이터 하나에 들어갈 음절?의 갯수. 
# 이거를 정해야하는데 이것도 정하는 아이디어가 있지만 완전히 정답이 있는 것은 아니고 우리는 하나의 배치 안에 100개의 샘플을 넣고
# 샘플 하나는 300 음절짜리 데이터가 들어간다고 하자.

batch_size = 100
seq_length = 300

num_batches = int(tensor.size / (batch_size * seq_length))

tensor = tensor[:num_batches * batch_size * seq_length] 
# cut remainings 남는얘들을 짜르는 것. 짜르기 싫으면 이 부분을 잘 수정해주면 된다.
# 텐서플로에서 배치 학습을 하기 위한 데이터 형식
char_dataset = tf.data.Dataset.from_tensor_slices(tensor)
type(char_dataset)
tensorflow.python.data.ops.dataset_ops.TensorSliceDataset
# 텐서플로에서 배치 학습을 하기 위한 데이터 형식
sequences = char_dataset.batch(seq_length+1, drop_remainder=True)
type(sequences)
tensorflow.python.data.ops.dataset_ops.BatchDataset
# RNN 학습을 위한 정답 데이터를 1칸씩 다음 결과를 targeting해서 제조
def split_input_target(chunk):
    input_text = chunk[:-1]
    target_text = chunk[1:]
    return input_text, target_text

dataset = sequences.map(split_input_target)
type(dataset)
tensorflow.python.data.ops.dataset_ops.MapDataset
# 데이터셋이 제대로 되어있는지 확인
for input_example, target_example in dataset.take(1):
    print(input_example.numpy())
    print(target_example.numpy())
[  2 157 178 138 129  28 136 160 129 138 178 133 129   2 137 178 150 129
  19  14   2  81  86  74  71  84  19  31 159 160 152 129 150 164 152 129
  14   2  84  70  19  31 147 165 129 157 165 138 129 130 165 129 152 173
 129 150 178 129  11   1  12   2 150 160 152 129 152 176 129 159 160 152
 129 146 168 130 129   2  28   2 146 160 129 133 166 129 130 172 129   1
  94  94  30  86  67  68  78  71  67  78  75  73  80  31  69  71  80  86
  71  84  32  30   5  40  40  40  40  40  40  32  30  28  32   1 157 178
 138 129  28 146 160 129 133 166 129 130 172 129  10  52  71  28   2 153
 165 129 138 168 129 147 173 129 157 164 129   2 150 180 129 153 160 130
 129 159 160 129 133 178 133 129   2 152 180 129 150 165 129 130 167 129
   2 150 161 152 129 159 169 138 129  11  11   1  32   2   2 157 178 138
 129  28 138 173 129 147 180 129  14 130 178 138 129 153 160 129  31  14
 138 173 129 147 180 129  31  11   1  32   2 136 160 152 129 150 180 133
 129  14   2 133 160 129 157 161 129 159 160 129 130 173 133 129 152 172
 129  33   1  32   2 157 178 138 129  28 138 173 129 147 180 129  14 130
 178 138 129 153 160 129  31  14 138 173 129 147 180 129  31  11 157 178
 138 129  28 138 173 129 147 180 129  14 130 178]
[157 178 138 129  28 136 160 129 138 178 133 129   2 137 178 150 129  19
  14   2  81  86  74  71  84  19  31 159 160 152 129 150 164 152 129  14
   2  84  70  19  31 147 165 129 157 165 138 129 130 165 129 152 173 129
 150 178 129  11   1  12   2 150 160 152 129 152 176 129 159 160 152 129
 146 168 130 129   2  28   2 146 160 129 133 166 129 130 172 129   1  94
  94  30  86  67  68  78  71  67  78  75  73  80  31  69  71  80  86  71
  84  32  30   5  40  40  40  40  40  40  32  30  28  32   1 157 178 138
 129  28 146 160 129 133 166 129 130 172 129  10  52  71  28   2 153 165
 129 138 168 129 147 173 129 157 164 129   2 150 180 129 153 160 130 129
 159 160 129 133 178 133 129   2 152 180 129 150 165 129 130 167 129   2
 150 161 152 129 159 169 138 129  11  11   1  32   2   2 157 178 138 129
  28 138 173 129 147 180 129  14 130 178 138 129 153 160 129  31  14 138
 173 129 147 180 129  31  11   1  32   2 136 160 152 129 150 180 133 129
  14   2 133 160 129 157 161 129 159 160 129 130 173 133 129 152 172 129
  33   1  32   2 157 178 138 129  28 138 173 129 147 180 129  14 130 178
 138 129 153 160 129  31  14 138 173 129 147 180 129  31  11 157 178 138
 129  28 138 173 129 147 180 129  14 130 178 138]
# 배치데이터 준비
dataset = dataset.shuffle(10000).batch(batch_size, drop_remainder=True)

# 글의 흐름이 순서가 있기 때문에 셔플을 하지 않고 학습을 하면 글의 흐름에 따라 학습내용이 영향을 받기 때문에
# 그것을 방지하고자 섞어준다. 물론 계속 재학습해주는 방법도 있다.
# 데이터가 준비되었으니 학습을 시키자

rnn_size  = 128
embed_dim = 128

model = tf.keras.Sequential([
        # 케라스의 sequential layer라고 해서 리스트로 모델을 넣어주면 알아서 모델 조합을 해준다.
    tf.keras.layers.Embedding(vocab_size, embed_dim, batch_input_shape=[batch_size, None]),
    # 포맷을 맞추기 위한 임베딩 레이어이다. 
    # 임베딩은 word2vec할때 임베딩을 말하는것이다. 쉽게말해서 데이터 형태를 맞추기 위한 어뎁터 같은 것이다.
    tf.keras.layers.LSTM(rnn_size, return_sequences=True, stateful=True, recurrent_initializer='glorot_uniform'),
    # LSTM은 RNN의 개선된 형태
    tf.keras.layers.Dense(vocab_size)
    # rogistic regression 같은 역할을 해주는 것이 dense 레이어이다.
])

# 모델이 잘 돌아가는지 테스트
for input_example_batch, target_example_batch in dataset.take(1): 
    example_batch_predictions = model(input_example_batch)
    print(example_batch_predictions.shape, "# (batch_size, sequence_length, vocab_size)")

# 로스 정의     
def loss(labels, logits):
    return tf.keras.losses.sparse_categorical_crossentropy(labels, logits, from_logits=True)

# 로스가 잘 찍히는지 테스트 
example_batch_loss  = loss(target_example_batch, example_batch_predictions)
print("Prediction shape: ", example_batch_predictions.shape, " # (batch_size, sequence_length, vocab_size)") 
print("scalar_loss:      ", example_batch_loss.numpy().mean())

# 모델 제작
model.compile(optimizer='adam', loss=loss)
(100, 300, 181) # (batch_size, sequence_length, vocab_size)
Prediction shape:  (100, 300, 181)  # (batch_size, sequence_length, vocab_size)
scalar_loss:       5.197217
import os
# Directory where the checkpoints will be saved
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt_{epoch}")

checkpoint_callback=tf.keras.callbacks.ModelCheckpoint(filepath=checkpoint_prefix,save_weights_only=True)

history = model.fit(dataset, epochs=1, callbacks=[checkpoint_callback])

# epochs를 숫자를 조절하면 그만큼 햑습량이 변동된다.
# 체크포인트 콜백이라고해서 에포크가 한번 돌때마다 체크포인트 콜벡에 저장이된다.
# 그래서 위에 정의한 디렉토리로 체크포인트가 저장될 것이다.
364/364 [==============================] - 233s 639ms/step - loss: 2.4221
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
model.build(tf.TensorShape([1, None]))
  • 여기서부터는 위에서 데이터를 가공하여 학습한 RNN 모델로 sentence를 생성해본다.
input_filename = 'namuwiki_20180326_charactor_mini_2.txt'
input_file = open(input_filename, 'r', encoding='utf8')
data = input_file.read()

import collections
counter = collections.Counter(data)
counter.items()

counter_item_sorted = sorted(counter.items(), key=lambda x: x[0])

chars = list(map(lambda x: x[0], counter_item_sorted))

vocab_size = len(chars)
vocab = dict(zip(chars, range(vocab_size)))
rnn_size = 128
embed_dim = 128
batch_size = 1

model = tf.keras.Sequential([
    tf.keras.layers.Embedding(vocab_size, embed_dim, 
                              batch_input_shape=[batch_size, None]),
    tf.keras.layers.LSTM(rnn_size, 
                        return_sequences=True, 
                        stateful=True, 
                        recurrent_initializer='glorot_uniform'),
    tf.keras.layers.Dense(vocab_size)
  ])
# 체크포인트에서 무작정 불러와서 모델을 쓸 수 있는게 아니라 
# 위에 코드와 같이 사전에 미리 모델을 정의해줘야한다.
# 모델을 정의한 다음에 체크포인트로 부터 저장된 학습내용을 불러올 수 있는 것이다.

import os

# Directory where the checkpoints will be saved
checkpoint_dir = './training_checkpoints'
tf.train.latest_checkpoint(checkpoint_dir)
'./training_checkpoints/ckpt_1'
model.load_weights(tf.train.latest_checkpoint(checkpoint_dir))
model.build(tf.TensorShape([1, None]))
def generate_text(model, start_string):
    # 모델이 1000개 길이 정도 되는 문장을 만들어 낼 것이다.
    num_generate = 1000

    # Converting our start string to numbers (vectorizing) 
    input_eval = [vocab[s] for s in start_string]
    input_eval = tf.expand_dims(input_eval, 0)

    # Empty string to store our results
    text_generated = []
    
    # 그 RNN을 설명하는 그림처럼 데이터를 넣어주고 아웃풋을
    # 또 다른 노드의 인풋값으로 집어넣고 이런 과정을
    # for문으로 반복해주는 코드이다.
    model.reset_states()
    for i in range(num_generate):
        predictions = model(input_eval)
        # predictions를 찍어보면 글자별로 조건부 확률이 쭉 출력될 것이다.
        # 조건부 확률중에 제일 큰것을 골라서 다음번의 인풋으로 집어넣는 작업을 할 것이다.
        
        # remove the batch dimension
        predictions = tf.squeeze(predictions, 0)

        predicted_id = tf.random.categorical(predictions, num_samples=1)[-1,0].numpy()
      
        # 나온 단어를 차원을 맞추어서 다음 입력으로 제공
        input_eval = tf.expand_dims([predicted_id], 0)
      
        text_generated.append(chars[predicted_id])

    return (start_string + ''.join(text_generated))

from Hangulpy3 import decompose_text, automata
# 아래에 "한국에서는" 이라는 문장을 뜯어서 처리해줘야 하는데
# 그것을 해주는게 hangulpy3다.
# 한국에서는 이라는 것을 뜯으면 ㅎ ㅏ ㄴ ㄱ ㅜ ㄱ 이런식으로 될 것이다.
# 아래 print로 찍은 것과 같다.

start_string = decompose_text("한국에서는")
# decompose_text하면 "한국에서는" 문장을 뜯는것이고
# automata를 하면 뜯어진 것을 다시 붙이는 작업을 해준다.

generated_text = generate_text(model, start_string=start_string)
print(generated_text[0:50])
print(' ')

print(automata("".join(generated_text)))
ㅎㅏㄴᴥㄱㅜㄱᴥㅇㅔᴥㅅㅓᴥㄴㅡㄴᴥㅈㅣᴥㄹㅏᴥ ㄷㅏᴥㅎㅣᴥㄹㅗᴥ .A= ㅇㅕㄹᴥㅇㅡᴥㄷㅏᴥㅊ
 
한국에서는지라 다히로 .A= 열으다차 '오슥해넜 마쥐으르
9강영 카여라 핸은)리하애
타되 강벙v51몬나른 작덕이으러가고 만층아 3106굴늘난그괴든눔뎐 웅망니 출으 억 2=	ㅇ우럽ㄴ얐 이우 친나니2요이가 봉다 절 불비레믈에ㄹ네 방도능 2소.Be, 에른세르기, 쵸종 월세저토 신 즌송개초도무른 푹엗 었로른외
*레, 1앗을 으그저로
.=늠위 웄어
8우일다겼(3회되갔타끼 졍좌영회 3여 슨해느 보랄 방항구볼 l> e거ㅏ해호다라하 든 갈 월곤북구시이 길 시 서ㅏ도느 되우청붕해께ㅆㄴ 삭유기거 1바
ㅣㅣㅇ죠푬홪바터도 화 달기 곰헝에 매=: 하 죄 각헉 연쇼어그하 데에 낵이 바마챈을 우얼하만 경예, 1러른 따 벽함기벙 주클모해ㅏ 삭얐 톼밈미컴 ㅏㅓ멱 버붕이가 간페 샜주 옹비괴 #선자ㅗ끼 한했든  ek=a* "에거소 즌윽긴긋

결론적으로는 형편없는 결과가 나오기는 했는데 데이터양과 학습량에 따라 모델성능 차이가 날 것이다.