HyeM

6부_6장 마르코프 체인과 LSTM으로 문장 생성하기 본문

Study/AI&DeepLearning

6부_6장 마르코프 체인과 LSTM으로 문장 생성하기

Hailey_HyeM207 2020. 8. 14. 02:55

01. 마르코프 체인과 LSTM/RNN

이번 챕터에서는 문장 자동 생성을 하는 것이 목표이다.

마르코프체인과 LSTM/RNN은 서로 다른 방식의 문장 생성 방식이다.

 

  • 마르코프체인 : 확률을 기반으로 문장을 이어 붙임
  • LSTM/RNN : 머신러닝으로 다음에 위치한 문장 예측하여 문장 생성.

 

02. 마르코프 체인

마르코프체인 (워드 샐러드) : 확률을 기반으로 하는 방법

  ->  마르코프체인을 이용하면 기존 문장을 기반으로 문장을 자동으로 생성가능함.

 

# 마르코프 성질이란 ?

- 의미 : 

 과거의 상태를 무시하고, 현재의 상태만을 기반으로 다음 상태를 선택하는 것

- 표현 :

   현재 상태 : qi     다음상태 : qj   

   다음상태로 이동할 확률 : P(qj | qi )        #현재상태와 다음상태을 기준으로 결정됨.

 

 

# 마르코프 체인의 기능 :

주기능 : 문장 생성 // 부기능 : 문장요약하기    ( 기계적으로 문장생성하고, twitter에 자동으로 등록하는 트위터 봇에 사용됨.)

 

# 문장 생성 과정  (예. 사전만들기_"그는 고양이를 좋아합니다")

  1.  문장을 단어로 분할 (형태소 분석)    (예. 그|는|고양이|를|좋아|합니다)
  2. 단어의 전후연결을 딕셔너리에 등록   
    (예.  그|는|고양이

          는|고양이|를
          고양이|를|좋아
          를|좋아|합니다.   )   <- N-gram과 다르게 문자가 아닌 단어 단위로 처리함
  3. 사전을 이용에 임의의 문장을 생성
    ( 예. *만약 "개"와 관련된 속담을 등록했다 가정*

       개|도|닷새|가|되면|주인|을|안다|.
       기르던|개|에게|다리|가|물렸다|.
       닭|쫓던|개|지붕|쳐다|보듯|한다|.
       똥|묻은|개|가|겨|묻은|개|나무란다|.

    "개"로 문장을 시작했다면, "개"뒤에 올 수있는 것은 "도/에게/지붕/가"이다/
    예를 들어 "가"를 연결하고 그 뒤를 마저 연결하면,
    "개 + 가 +되면 + 주인 + 을 + 안다 + . "라는 독창적인 문장이 만들어진다.

=> 마르코프체인은 단어의 실질적인 의미 연관성을 생각하지 않음.  -> 이상한 문장이 만들어지기도 한다.

 

 

 

[실습1] 마르코프체인 구현

실제로 마르코프 체인으로 문장을 만드는 프로그램을 짜본다. 

- 코드 흐름: 형태소 분석(koNLPy) -> 사전 만들기 -> 사전 기반 문장 만들기

- 테스트 파일 : "토지"의 텍스트 파일

- 띄어쓰기 규칙 : 한국어의 띄어쓰기 규칙은 복잡하므로, 네이버 맞춤법 검사기를 이용한다.

 

<코드 : markov.py >

import os
import codecs
from bs4 import BeautifulSoup
from konlpy.tag import Twitter
import urllib.request
import os, re, json, random

#현재 디렉토리 변경하기
os.chdir('C:/Users/xxx/Desktop/python 코드/machineLearning/chatbot6')

#네이버 맞춤법 검사 요청에 user-agent 헤더 추가
import requests
headers = {
    'User-Agent' : 'Mozilla/5.0 (Windows; U; Windows NT 6.1; ko; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 IPMS/A640400A-14D460801A1-000000426571',   # firefox UserAgent
}

# 마르코프 체인 딕셔너리 만들기 --- (※1)
def make_dic(words):
    tmp = ["@"] #문장의 시작은 @
    dic = {} #사전은 딕셔너리 자료형
    for word in words:  
        tmp.append(word)
        if len(tmp) < 3: continue  #세 단어가 한 세트
        if len(tmp) > 3: tmp = tmp[1:] 
        set_word3(dic, tmp) #함수 호출
        if word == ".":
            tmp = ["@"]
            continue
    return dic

# 딕셔너리에 데이터 등록하기 --- (※2)
def set_word3(dic, s3):
    w1, w2, w3 = s3
    if not w1 in dic: dic[w1] = {}
    if not w2 in dic[w1]: dic[w1][w2] = {}
    if not w3 in dic[w1][w2]: dic[w1][w2][w3] = 0
    dic[w1][w2][w3] += 1 


# 문장 만들기 --- (※3)
def make_sentence(dic):
    ret = [] 
    if not "@" in dic: return "no dic" 
    top = dic["@"]
    w1 = word_choice(top) #단어 무작위 가져옴.
    w2 = word_choice(top[w1])
    ret.append(w1)
    ret.append(w2)
    while True:
        w3 = word_choice(dic[w1][w2])
        ret.append(w3)
        if w3 == ".": break
        w1, w2 = w2, w3
    ret = "".join(ret)
    # 띄어쓰기
    params = urllib.parse.urlencode({ #params는 뒤에서 맞춤법 검사기에 들어간다. 
                #urllib.parse.urlencode : str(byte)객체를 포함할 수 있는 매핑 객체나 두 요소 튜플의 시퀀스를 아스키텍스트 "문자열"로 변환한다. 
        "_callback": "", #callback : 다른 코드의 인수로서 넘겨주는 실행 가능한 코드
        "q": ret 
    })

    # 네이버 맞춤법 검사기를 사용합니다.
    data = urllib.request.urlopen("https://m.search.naver.com/p/csearch/ocontent/spellchecker.nhn?" + params) #책의 url을 바꿈
    data = data.read().decode("utf-8")[1:-2]
    data = json.loads(data)
    data = data["message"]["result"]["html"]
    data = soup = BeautifulSoup(data, "html.parser").getText()
    # 리턴
    return data

def word_choice(sel):
    keys = sel.keys()
    return random.choice(list(keys)) #sel의 key값들 중 랜덤으로 1개 고름


# 문장 읽어 들이기 --- (※4)
toji_file = "toji.txt"
dict_file = "markov-toji.json"
if not os.path.exists(dict_file):  #딕셔너리 파일이 없다면, 아래 코드로 만듦
    # 토지 텍스트 파일 읽어 들이기
    fp = codecs.open("BEXX0003.txt", "r", encoding="utf-16")
    soup = BeautifulSoup(fp, "html.parser")
    body = soup.select_one("body > text")
    text = body.getText()
    text = text.replace("…", "") # 현재 koNLPy가 …을 구두점으로 잡지 못하는 문제 임시 해결
    # 형태소 분석
    twitter=okt()   #원래는  # twitter = Twitter() 인데, okt가 최신 코드임.
    malist = twitter.pos(text, norm=True) #형태소 분석
    words = []
    for word in malist:
        # 구두점 등은 대상에서 제외(단 마침표는 포함)
        if not word[1] in ["Punctuation"]: 
            words.append(word[0])
        if word[0] == ".":
            words.append(word[0]) 
    # 딕셔너리 생성
    dic = make_dic(words) #함수호출
    json.dump(dic, open(dict_file,"w", encoding="utf-8")) #사전 dic을 dict_file이름으로 저장(생성)

else: #딕셔너리가 존재한다면, load함. 
    dic = json.load(open(dict_file,"r"))


# 문장 만들기 --- (※6)
for i in range(3):
    s = make_sentence(dic)  #함수호출
    print(s)
    print("---")

-  !코드변경!

   1. 네이버 맞춤법 사이트 url 변경 

   2. 네이버 맞춤법 검사 요청에서 User-Agent헤더 추가 (https://www.askcompany.kr/vod/crawling/51/)

 

- 코드 구성 :

  • make_dic(words) 함수
  • set_word3(dic, s3) 함수
  • make_sentence(dic) 함수
  • word_choice(sel) 함수
  • 문장 읽어들이기 
  • 문장 만들기

- 코드 흐름 :

  1.   문장 읽어들이기(#4)
  2.   딕셔너리 파일이 없다면, 형태소 분석후 딕셔너리를 생성한다.(make_dic호출)
  3.  make_dic함수에서는 문장을 읽어들이고, 세 단어가 한 세트가 되도록 하여 딕셔너리에 데이터를 등록한다. (set_word3호출)
  4.  딕셔너리 생성완료함.  (오른쪽 캡쳐 : 딕셔너리  구조)
  5.  문장을 만든다. (make_sentence호출)
  6. make_sentence에서는 단어를 무작위로 가져와(word_choice함수)  단어를 구성하고,
    띄어쓰기를 해주고, 네이버 맞춤법 검사기를 이용하여 문장을 만든다. 

 

 

- 실행화면 :  어색한 문장들이  생성된 것을 확인할 수 있다. 

실행1
실행2
실행 후, 새로 생긴 딕셔너리 파일

 

 

03. LSTM/RNN

RNN(Recurrent Neural Network) : 재귀신경망; 신경망을 재귀적으로 사용해 시간 순서를 가진 데이터를 다룸

KSTM(Long Short Term-Memory) : RNN을 개량한 것으로, 장기적으로 데이터를 기억할 수 있게 여러가지 기능을 추가함.

 

-> 시간 순서를 기반으로 데이터를 다룰 수 있어, 문장 쉽게 생성 가능하다.

(Ex. '오늘'이라고 입력시, '아침','날씨'등이 이어질 것으로 예상하고 조합할 수 있다. )

 

 

 

[실습1] 마르코프체인 구현

keras의 샘플 중 "lstm_text_generation.py"(문장을 자동생성하는 프로그램) 라는 파일을 수정하여 문장을 자동 생성하는 프로그램을 짜본다. (LSTM)

- 테스트 파일 : "토지"의 텍스트 파일

<코드 : lstm-text-gen.py >

import codecs
from bs4 import BeautifulSoup
from keras.models import Sequential
from keras.layers import Dense, Activation, Dropout
from keras.layers import LSTM
from keras.optimizers import RMSprop
from keras.utils.data_utils import get_file
import numpy as np
import random, sys

#'토지'파일 읽어오기_(#1)
fp = codecs.open("./BEXX0003.txt", "r", encoding="utf-16")
soup = BeautifulSoup(fp, "html.parser")
body = soup.select_one("body")
text = body.getText() + " "
print('코퍼스의 길이: ', len(text))

# 문자를 하나하나 읽어 들이고 ID 붙이기_(#2)
chars = sorted(list(set(text)))
print('사용되고 있는 문자의 수:', len(chars))
char_indices = dict((c, i) for i, c in enumerate(chars)) # 문자 → ID
indices_char = dict((i, c) for i, c in enumerate(chars)) # ID → 문자

# 텍스트를 maxlen개의 문자로 자르고 다음에 오는 문자 등록하기 _(#3)
maxlen = 20
step = 3
sentences = []
next_chars = []
for i in range(0, len(text) - maxlen, step):
    sentences.append(text[i: i + maxlen])
    next_chars.append(text[i + maxlen])  #텍스트를 maxlen개의 문자로 자름.
print('학습할 구문의 수:', len(sentences))

print('텍스트를 ID 벡터로 변환합니다...')
X = np.zeros((len(sentences), maxlen, len(chars)), dtype=np.bool) #np.zeros : 0으로 초기화된 shape차원의 ndarray배열 객체를 반환한다.
y = np.zeros((len(sentences), len(chars)), dtype=np.bool)
for i, sentence in enumerate(sentences):  #enumerate : return값으로 인덱스를 포함하는 enumerate객체를 반환한다.
    for t, char in enumerate(sentence):
        X[i, t, char_indices[char]] = 1
    y[i, char_indices[next_chars[i]]] = 1

# 모델 구축하기(LSTM)
print('모델을 구축합니다...')
model = Sequential()  #모델은 Sequential 
model.add(LSTM(128, input_shape=(maxlen, len(chars))))
model.add(Dense(len(chars)))
model.add(Activation('softmax'))
optimizer = RMSprop(lr=0.01)
model.compile(loss='categorical_crossentropy', optimizer=optimizer)


# 후보를 배열에서 꺼내기
def sample(preds, temperature=1.0):
    preds = np.asarray(preds).astype('float64')
    preds = np.log(preds) / temperature
    exp_preds = np.exp(preds)
    preds = exp_preds / np.sum(exp_preds)
    probas = np.random.multinomial(1, preds, 1)
    return np.argmax(probas)

# 학습시키고 텍스트 생성하기 반복
for iteration in range(1, 60):
    print()
    print('-' * 50)
    print('반복 =', iteration)
    model.fit(X, y, batch_size=128, nb_epoch=1) # 
    # 임의의 시작 텍스트 선택하기
    start_index = random.randint(0, len(text) - maxlen - 1)
    # 다양한 다양성의 문장 생성
    for diversity in [0.2, 0.5, 1.0, 1.2]:
        print()
        print('--- 다양성 = ', diversity)
        generated = ''
        sentence = text[start_index: start_index + maxlen]
        generated += sentence
        print('--- 시드 = "' + sentence + '"')
        sys.stdout.write(generated)
        # 시드를 기반으로 텍스트 자동 생성
        for i in range(400):
            x = np.zeros((1, maxlen, len(chars)))
            for t, char in enumerate(sentence):
                x[0, t, char_indices[char]] = 1.
            # 다음에 올 문자를 예측하기
            preds = model.predict(x, verbose=0)[0]
            next_index = sample(preds, diversity)
            next_char = indices_char[next_index]
            # 출력하기
            generated += next_char
            sentence = sentence[1:] + next_char
            sys.stdout.write(next_char)
            sys.stdout.flush()
        print()

 

- 실행화면 : 위의 마르코프체인으로 만든 문장 보단 더 자연스러운 것을 확인가능하다. 

(직접하는 코드 실행은 툴 다운오류로 생략_ 책 실행결과로 대체)

실행
실행하면 생성되는 문장들

 

 

 

 

 

위 글은 [(파이썬을 이용한)머신러닝, 딥러닝 실전 개발 입문] 을 읽고 정리한 글입니다.

Comments