#!/usr/bin/env python # coding: utf-8 # # A(부록) 순환신경망과 LSTM # # 순환신경망을 텐서프로로 구현해보록 부록준비 # # 합성곱신경망은 이미지들 간의 변화를 예측하고자 할 때는 적용하기 어려움. # # 연속된 데이터를 이용하여 앞으로 다가올 데이터를 추측, 새로운 데이터에 대해 효과적 판단을 필요로 하는 문제에 적합. # # 예시: 언어모델링, 자연어처리, 번역 등 # ## A.1 순환 신경망 알고리즘 # # 순환신경망 Recurrent Neural Network(RNN) # # 순환의 의미: 신경망의 뉴런에서 나온 정보가 다시 재사용되도록 순환됨. # # 이전의 데이터를 사용하여 학습된 뉴런의 어떤 상태 정보가 다음 데이터를 이용하여 뉴런을 학습시킬 때 다시 사용. # # 메모리 셀(memory cell), 셀(cell): 순환신경망에서 사용하는 뉴런. # # 은닉 상태 hidden state # # 셀에서 만들어지는 상태 데이터 # # 이전의 은닉 상태와 현재 입력 값을 어떤 가중치 W로 곱하고 편향 값 b를 더하는 것. # # \begin{equation*} # \ h_t = tanh(W(h_{t-1}, x_t) + b) # \end{equation*} # ## A.2 LSTM 순환 신경망 알고리즘 # # 기본적인 순환 신경망은 단기기억(short-term memory)을 저장할 수 있다. # # 기본적인 순환 신경망은 비교적 짧은 거리에 문맥의 정보를 실어 나름. # # => 멀리 떨어진 데이터 간의 연관정보 파악이 어려움. # # LSTM(long short-term memory) 순환 신경망 알고리즘 # # 단기기억을 더 길게 유지시켜주는 알고리즘. # # 은닉상태와 셀 상태(cell state) 두 가지를 계산. # # 은닉상태(h)는 상위 계층의 입력으로 전달. # # 셀 상태(c)는 상위 계층으로 전달되지 않음. # # \begin{equation*} # \ p = W(h_{t-1}, x_t) + b # \end{equation*} # # 이전 셀의 은닉 상태와 현재 입력 값에 가중치를 곱한 선형 계산의 결과 = p # # 삭제게이트: 이전 셀의 값 중 삭제해야 할 정보를 학습하기 위한 것. # # 입력게이트: 새롭게 추가되어야 할 정보를 학습하게 도와 줌. # # \begin{equation*} # \ f_t = c_{t-1} * sigmoid(p_f) # \end{equation*} # # 삭제게이트는 이전 셀 상태와 p에 시그모이드 활성화 함수를 적용한 것을 곱해 계산 # # \begin{equation*} # \ i_t = sigmoid(p_i) * tanh(p_j) # \end{equation*} # # 입력게이트는 p에 시그모이드 활성화 함수를 적용한 것과 p에 tanh를 적용한 것을 곱해 계산 # # \begin{equation*} # \ c_t = f_t + i_t # \end{equation*} # # 현재 셀 상태는 삭제 게이트 결과와 입력 게이트 결과를 더해서 구함. # # \begin{equation*} # \ h_t = tanh(c_t) * sigmoid(p_o) # \end{equation*} # # 은닉상태 계산은 현재의 셀 상태에 tanh 활성화 함수 적용 * p에 시그모이드 활성화 함수를 적용 # ## A.3 오버피팅 문제 # # 드롭아웃 # # 일반적인 피드포워드(feed forward) 신경망에서 모델이 학습 데이터에 과다하게 치중되어 만들어지는 것을 막아주는 좋은 방법 # # 순환 신경망의 출력 값의 흐름 중 수직 방향에 대해서만 드롭아웃을 적용. # ## A.4 언어 모델링 # # 텐서플로 공식 튜토리얼 순환신경망 문서 # # https://www.tensorflow.org/versions/master/tutorials/recurrent/ # # 팬 트리 뱅크(Penn Treebank) 데이터 셋을 이용하여 언어 모델을 학습 # # 텐서플로 model 깃허브 소스: https://github.com/tensorflow/models/tree/0d9a3abdca7be4a855dc769d6d441a5bfcb77c6d/tutorials/rnn/ptb # # 학습에 필요한 데이터 다운로드 #
# wget http://www.fit.vutbr.cz/~imikolov/rnnlm/simple-examples.tgz
# tar xvf simple-examples.tgz
# 
# # ptb_word_lm.py: 텐서플로 모델을 만들고 학습시키는 주요 파일 # # reader.py: 학습데이터를 읽거나 전처리하는 데 사용하는 유틸리티 파일 # # ptb.train.txt: 모델을 학습시킬 때 사용 # # ptb.valid.txt: 모델을 검증 # # ptb.test.txt: 모델의 성능을 평가 # ## A.5 클래스 설정 # # GPU가 없는 개인용 컴퓨터에서 예제를 실행하려면 SmallConfig 설정을 선택하는 것을 권장 #
# class SmallConfig(object):
#   """Small config."""
#   init_scale = 0.1
#   learning_rate = 1.0
#   max_grad_norm = 5
#   num_layers = 2
#   num_steps = 20
#   hidden_size = 200
#   max_epoch = 4
#   max_max_epoch = 13
#   keep_prob = 1.0
#   lr_decay = 0.5
#   batch_size = 20
#   vocab_size = 10000
# 
# * 코드 설명 # # init_scale: 가중치 행렬을 랜덤하게 초기화할 때 생성되는 값의 범위 지정 # # learning_rate: 경사 하강법을 사용할 때 학습 속도를 조절하기 위한 변수 # # 학습속도: learning_rate * lr_decay를 곱해 구해짐. lr_decay가 학습이 반복될 때마다 작아짐 # # max_grad_norm: 구해진 기울기(gradient) 값이 과다하게 커지는 것을 막아주기 위한 상한 설정하는데 사용 # # num_layers: 순환 신경망을 구성할 계층의 개수 # # hidden_size: 한 계층에 배치할 뉴런의 수 # # 순환신경망 전체 뉴런수 = num_layers(2) * hidden_size(200) = 400 # # num_step: 셀의 가중치를 학습시킨 후 경사 하강법을 사용하여 기울기를 업데이트 # # keep_prob: 드롭아웃하지 않을 확률을 지정 # # batch_size: 미니배치 데이터 크기를 지정 # ## A.6 학습 데이터 # # reader.py ptb_raw_data 함수를 사용하여 데이터를 읽는 것으로 시작 # # 단어가 나타나는 횟수가 높은 순으로 저열 # # 정렬된 단어 순으로 차례대로 번호를 부여 # # ptb 관련 txt에 나타나는 단어를 모두 숫자로 바꿀 수 있음 # # ptp_raw_data를 통해 받는 데이터는 단어를 숫자로 바꾼 리스트 # # ptb_iterator: 학습 데이터를 배치 개수로 나누어 num_step 크기만큼 나누어 읽어오는 역할 # # 순환 신경망에서는 순차적인 데이터를 처리해야하기 때문에 미니 배치 데이터 추출 방법을 조금 다르게 처리 # # 예시) 문장 단위로 나누어 임의의 순서대로 섞을 수 있음 # ## A.7 모델 생성 클래스 # # 학습, 검증, 테스트에 모두 동일한 모델을 사용 # # PTBModel 클래스를 만들어 설정을 바꾸어가며 학습, 검증, 테스트에서 사용 # # __init__에서 텐서플로를 사용하여 신경망 모델을 모두 구성 # # BasicSTMCell 클래스를 이용하여 LSTM 셀을 생성 # # MultiRNNCell 클래스를 이용해 두 개의 계층을 가진 순환 신경망을 만듬 # # 단어의 벡터 표현(word2vec)을 만들기 위해 단어 임베딩(embedding) 작업을 수행 # # https://www.tensorflow.org/versions/master/tutorials/word2vec/ # # state_is_tuple을 True로 설정. 셀 상태와 은닉 상태 데이터를 튜플로 관리 # # BasicSTMCell 셀 상태, 은닉 상태, 각 셀에서 계산할 가중치 행렬 모두 관리 # # 입력 데이터가 20개의 배치에서 하나씩 선택되어 전달 # # 셀의 상태 데이터도 그에 따라 20개의 배치에 대응하여 상태 데이터가 섞이지 않도록 따로 구분하여 저장 #
#         # 각 배치마다 순서대로 데이터를 뽑아 셀에 입력합니다. 
#         outputs = []
#         state = self.initial_state
#         with tf.variable_scope("RNN"):
#             for time_step in range(config.num_steps):
#                 if time_step > 0: tf.get_variable_scope().reuse_variables()
#                 (cell_output, state) = cell(inputs[:, time_step, :], state)
#                 outputs.append(cell_output)
# 
# cell_output 값은 소프트맥스 함수를 거쳐 만 개의 단어 중 어떤 단어를 예측하는 결과를 만듬. # # sequence_loss_by_example을 사용하여 소프트맥스 함수와 크로스 엔트로피 계산을 처리. #
#         # 기울기 클리핑을 수행합니다.
#         grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars),
#                                           config.max_grad_norm)
#         optimizer = tf.train.GradientDescentOptimizer(self.lr)
#         self.train_op = optimizer.apply_gradients(zip(grads, tvars))
# 
# * 코드 설명 # # 경사 하강법으로 구해진 기울기를 config.max_grad_norm 기준으로 클리핑. # # 경사 하강법 최적화 객체를 생성해서 모델의 매개변수를 학습시킴 # ## A.8 반복 함수 # # run_epoch: 텐서플로 모델을 실행시키고 셀에서 나온 은닉 상태와 셀 상태의 데이터를 업데이트하는 역할 # # max_max_epoch 값만큼 반복을 진행. 오차를 최소화. # # session.run() 메서드에 들어갈 매개변수는 cost, train_op, final_state 튜플에 담겨 있는 두 계층의 셀 상태와 은닉 상태 데이터임. # ## A.9 결과 # # 학습의 결과는 언어 모델링에서 자주 사용되는 혼잡도(perplexity)를 계산하여 평가 # # 혼잡도(perplexity): 배치 반복 안에서 누적된 오차 값을 진행된 num_steps를 합한 값으로 나눈 것. # # 소스참조: https://github.com/rickiepark/first-steps-with-tensorflow/blob/master/rnn_ptb/ptb_word.ipynb # In[1]: import time import numpy as np import tensorflow as tf from tensorflow.python.ops.rnn_cell import BasicLSTMCell, MultiRNNCell, DropoutWrapper #from tensorflow.models.rnn.ptb import reader # 텐서플로우 0.12 기준으로 reader.ptb_iterator 오류 발생. import reader # 오류 발생시키지 않도록 하기 위해 reader.ptb_iterator를 사용가능한 reader 모듈로 변경. # Small Config 정보를 사용합니다. # In[2]: class SmallConfig(object): """Small config.""" init_scale = 0.1 learning_rate = 1.0 max_grad_norm = 5 num_layers = 2 num_steps = 20 hidden_size = 200 max_epoch = 4 max_max_epoch = 13 keep_prob = 1.0 lr_decay = 0.5 batch_size = 20 vocab_size = 10000 # 트레이닝과 테스트에 사용할 두개의 config 오브젝트를 만듭니다. # In[3]: config = SmallConfig() eval_config = SmallConfig() eval_config.batch_size = 1 eval_config.num_steps = 1 # PTB 모델을 만들어 주는 클래스를 작성합니다. # In[4]: class PTBModel(object): """The PTB model.""" def __init__(self, config, is_training=False): self.batch_size = config.batch_size self.num_steps = config.num_steps input_size = [config.batch_size, config.num_steps] self.input_data = tf.placeholder(tf.int32, input_size) self.targets = tf.placeholder(tf.int32, input_size) lstm_cell = BasicLSTMCell(config.hidden_size, forget_bias=0.0, state_is_tuple=True) # SmallConfig에서는 드롭아웃이 적용되지 않습니다. if is_training and config.keep_prob < 1: lstm_cell = DropoutWrapper(lstm_cell, config.keep_prob) # 두개의 계층을 가진 신경망 구조를 만듭니다. cell = MultiRNNCell([lstm_cell] * config.num_layers, state_is_tuple=True) self.initial_state = cell.zero_state(config.batch_size, tf.float32) with tf.device("/cpu:0"): embedding_size = [config.vocab_size, config.hidden_size] embedding = tf.get_variable("embedding", embedding_size) inputs = tf.nn.embedding_lookup(embedding, self.input_data) # SmallConfig에서는 드롭아웃이 적용되지 않습니다. if is_training and config.keep_prob < 1: inputs = tf.nn.dropout(inputs, config.keep_prob) # 각 배치마다 순서대로 데이터를 뽑아 셀에 입력합니다. outputs = [] state = self.initial_state with tf.variable_scope("RNN"): for time_step in range(config.num_steps): if time_step > 0: tf.get_variable_scope().reuse_variables() (cell_output, state) = cell(inputs[:, time_step, :], state) outputs.append(cell_output) # output의 크기를 20x20x200에서 400x200으로 변경합니다. output = tf.reshape(tf.concat(1, outputs), [-1, config.hidden_size]) softmax_w_size = [config.hidden_size, config.vocab_size] softmax_w = tf.get_variable("softmax_w", softmax_w_size) softmax_b = tf.get_variable("softmax_b", [config.vocab_size]) # logits의 크기는 400x10000이 됩니다. logits = tf.matmul(output, softmax_w) + softmax_b loss = tf.nn.seq2seq.sequence_loss_by_example( [logits], [tf.reshape(self.targets, [-1])], [tf.ones([config.batch_size * config.num_steps])]) self.cost = tf.reduce_sum(loss) / config.batch_size self.final_state = state if not is_training: return self.lr = tf.Variable(0.0, trainable=False) tvars = tf.trainable_variables() # 기울기 클리핑을 수행합니다. grads, _ = tf.clip_by_global_norm(tf.gradients(self.cost, tvars), config.max_grad_norm) optimizer = tf.train.GradientDescentOptimizer(self.lr) self.train_op = optimizer.apply_gradients(zip(grads, tvars)) def assign_lr(self, session, lr_value): session.run(tf.assign(self.lr, lr_value)) # 에포크를 처리할 함수를 만듭니다. # In[5]: def run_epoch(session, m, data, is_training=False): """Runs the model on the given data.""" epoch_size = ((len(data) // m.batch_size) - 1) // m.num_steps start_time = time.time() costs = 0.0 iters = 0 eval_op = m.train_op if is_training else tf.no_op() # initial_state는 2x20x200 크기의 튜플입니다. state_list = [] for c, h in m.initial_state: state_list.extend([c.eval(), h.eval()]) ptb_iter = reader.ptb_iterator(data, m.batch_size, m.num_steps) for step, (x, y) in enumerate(ptb_iter): fetch_list = [m.cost] # final_state 튜플에 담겨있는 상태를 꺼내어 fetch_list에 담습니다. for c, h in m.final_state: fetch_list.extend([c, h]) fetch_list.append(eval_op) # 이전 스텝에서 구해진 state_list가 feed_dict로 주입됩니다. feed_dict = {m.input_data: x, m.targets: y} for i in range(len(m.initial_state)): c, h = m.initial_state[i] feed_dict[c], feed_dict[h] = state_list[i*2:(i+1)*2] # fetch_list에 담긴 final_state의 결과를 state_list로 전달 받습니다. cost, *state_list, _ = session.run(fetch_list, feed_dict) costs += cost iters += m.num_steps if is_training and step % (epoch_size // 10) == 10: print("%.3f perplexity: %.3f speed: %.0f wps" % (step * 1.0 / epoch_size, np.exp(costs / iters), iters * m.batch_size / (time.time() - start_time))) return np.exp(costs / iters) # In[6]: raw_data = reader.ptb_raw_data('simple-examples/data') train_data, valid_data, test_data, _ = raw_data # In[7]: with tf.Graph().as_default(), tf.Session() as session: initializer = tf.random_uniform_initializer(-config.init_scale, config.init_scale) # 학습과 검증, 테스트를 위한 모델을 만듭니다. with tf.variable_scope("model", reuse=None, initializer=initializer): m = PTBModel(config, is_training=True) with tf.variable_scope("model", reuse=True, initializer=initializer): mvalid = PTBModel(config) mtest = PTBModel(eval_config) tf.global_variables_initializer().run() for i in range(config.max_max_epoch): # lr_decay는 반복속도를 조절해 주는 역할을 합니다. lr_decay = config.lr_decay ** max(i - config.max_epoch, 0.0) m.assign_lr(session, config.learning_rate * lr_decay) print("Epoch: %d Learning rate: %.3f" % (i + 1, session.run(m.lr))) perplexity = run_epoch(session, m, train_data, is_training=True) print("Epoch: %d Train Perplexity: %.3f" % (i + 1, perplexity)) perplexity = run_epoch(session, mvalid, valid_data) print("Epoch: %d Valid Perplexity: %.3f" % (i + 1, perplexity)) perplexity = run_epoch(session, mtest, test_data) print("Test Perplexity: %.3f" % perplexity) # ## A.10 텐서플로가 제공하는 LSTM 이외의 모델 # # 핍홀(peephole) LSTM # # ftp://ftp.idsia.ch/pub/juergen/TimeCount-IJCNN2000.pdf # # 입력게이트 pi를 계산할 때 입력 데이터(xt)와 이전의 은닉 상태 데이터(ht-1) 외에 이전의 셀 상태(ct-1)도 함께 사용하는 방법 # # GRU(Gated Recurrent Unit) # # https://arxiv.org/pdf/1406.1078v3.pdf # # 뉴욕대학교 조경현 교수가 발표 # # 입력 게이트와 삭제 게이트를 하나의 업데이트 게이트로 묶음. # # 셀 상태와 은닉 상태를 합친 것이 특징.