본문 바로가기
정리/텐서플로와 머신러닝으로 시작하는 자연어처리

Chap04. 영어 텍스트 분류_모델링 2

by 스꼬맹이브로 2021. 7. 29.
728x90
반응형
SMALL

이전 포스팅에서는 머신러닝 모델을 사용하여 감정 분석을 수행하였다.

 

Chap04. 텍스트 분류_모델링 1

이전 포스팅에서는 탐색적 데이터 분석 과정과 데이터 전처리 과정에 대해 알아보았다. Chap04. 텍스트 분류_데이터전처리 실습 텍스트 분류란 자연어 처리 기술을 활용해 글의 정보를 추출해서

yuna96.tistory.com

이번 포스팅은 딥러닝 분야의 순환 신경망에 대해 실습하면서 알아보자.

 

1. 순환 신경망 분류 모델

순환 신경망(RNN)은 언어 모델에서 많이 쓰이는 모델 중 하나로 다른 모델들과 달리 이미 주어진 단어 특징 벡터를 활용해 모델을 학습하지 않고 텍스트 정보를 입력해서 문장에 대한 특징 정보를 추출한다.

이 모델이 나타내는 현재 정보는 이전 정보가 점층적으로 쌓이면서 정보를 표현할 수 있는 모델이다.

RNN 모델을 시각화한 그림은 다음과 같다.

RNN 도식화 예시("아버지 가방에 들어가신다")

이 모델은 한 단어에 대한 정보를 입력하면 다음에 나올 단어를 맞추는 모델로 '가방에' 라는 정보가 입력되었다면 이 한 단어만 가지고 예측하는 것이 아닌 앞 단어인 '아버지'라는 정보와 같이 처리하여 다음 단어를 예측하게 된다.

여기서 현재 정보인 '가방에'를 입력 상태(input state)라 부르고 이전 정보를 은닉 상태(hidden state)라고 부른다.

순환 신경망 RNN은 이 두 상태 정보를 활용해 순서가 있는 데이터에 대한 예측 모델링을 가능하게 한다.

 

이 모델을 활용하여 영화 평점 예측을 수행하기 위해 만들고자 하는 모델의 형태는 다음과 같다.

영화 평점 예측을 위한 순환 신경망 모델

이 모델은 입력 문장을 순차적으로 입력만 하고 마지막으로 입력한 시점에 출력 정보를 뽑아 영화 평점을 예측한다.

 

먼저 모델 구현을 위해 필요한 라이브러리와 전처리한 학습 데이터를 불러온다.

import tensorflow as tf
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split

import os
import json

DATA_IN_PATH = './data_in/'
DATA_OUT_PATH = './data_out/'

TRAIN_INPUT_DATA = 'train_input.npy'
TRAIN_LABEL_DATA = 'train_label.npy'
DATA_CONFIGS = 'data_configs.json'

input_data = np.load(open(DATA_IN_PATH + TRAIN_INPUT_DATA, 'rb'))
label_data = np.load(open(DATA_IN_PATH + TRAIN_LABEL_DATA, 'rb'))
prepro_configs = None

with open(DATA_IN_PATH + DATA_CONFIGS, 'r') as f:
    prepro_configs = json.load(f)

이전 포스팅에서 전처리 한 데이터를 사용할 것이며 넘파이 형태로 저장했기 때문에 np.load함수를 통해 데이터를 구성한다.

여기서 입력 데이터는 input_data 변수에, 정답 레이블 데이터는 label_data 변수에 저장한다.

 

다음으로 모델 하이퍼파라미터를 튜닝하기 위해 검증 데이터셋을 분리한다.

TEST_SPLIT = 0.1
RANDOM_SEED = 13371447

train_input, test_input, train_label, test_label = train_test_split(input_data, label_data,  test_size=TEST_SPLIT, random_state=RANDOM_SEED)

분리하기 위해 train_test_split함수를 사용하였으며 이 함수의 인자로 입력 데이터와 레이블 데이터, 그리고 검증 데이터셋을 어느 정도로 분리할 것인지 비율로 입력해준다.

 

이렇게 학습과 평가에 사용될 데이터셋이 준비되었으면 데이터 입력 함수를 구현한다.

데이터 입력 함수는 모델에 학습 데이터를 주입하는 방법을 나타낸다.

BATCH_SIZE = 16
NUM_EPOCHS = 3

def mapping_fn(X, Y):
    inputs, labels = {'x': X}, Y
    return inputs, labels

def train_input_fn():
    dataset = tf.data.Dataset.from_tensor_slices((train_input, train_label))
    dataset = dataset.shuffle(buffer_size=50000)
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.repeat(count=NUM_EPOCHS)
    dataset = dataset.map(mapping_fn)
    iterator = dataset.make_one_shot_iterator()
    
    return iterator.get_next()

def eval_input_fn():
    dataset = tf.data.Dataset.from_tensor_slices((test_input, test_label))
    dataset = dataset.map(mapping_fn)
    dataset = dataset.batch(BATCH_SIZE * 2)
    iterator = dataset.make_one_shot_iterator()
    
    return iterator.get_next()

데이터 입력 함수에서는 tf.data를 활용해 입력 데이터를 하나의 파이프라인을 거쳐 모델에 입력한다.

 

다음으로 모델을 구현해보자.

tf.estimator를 활용하기 위해서는 모델 그래프에 대한 정의를 함수에 구현해야 한다.

 

먼저 모델 하이퍼파라미터를 정의해보자.

VOCAB_SIZE = prepro_configs['vocab_size']+1
WORD_EMBEDDING_DIM = 100
HIDDEN_STATE_DIM = 150
DENSE_FEATURE_DIM = 150

learning_rate = 0.001

이전에 저장해두었던 단어 사전에 대한 크기와 모델에 대한 하이퍼파라미터인 각 변수의 차원과 학습률을 정한다.

다음으로 모델 함수에 들어갈 내용을 구현해보자.

# 모델 생성
def model_fn(features, labels, mode):
    TRAIN = mode == tf.estimator.ModeKeys.TRAIN
    EVAL = mode == tf.estimator.ModeKeys.EVAL
    PREDICT = mode == tf.estimator.ModeKeys.PREDICT
    
    # 1번째 설명
    embedding_layer = tf.keras.layers.Embedding(
                    VOCAB_SIZE,
                    WORD_EMBEDDING_DIM)(features['x'])
    
    embedding_layer = tf.keras.layers.Dropout(0.2)(embedding_layer)
    
    # 2번째 설명
    rnn_layers = [tf.nn.rnn_cell.LSTMCell(size) for size in [HIDDEN_STATE_DIM, HIDDEN_STATE_DIM]]
    multi_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layers)
	
    # 3번째 설명
    outputs, state = tf.nn.dynamic_rnn(cell=multi_rnn_cell,
                                       inputs=embedding_layer,
                                       dtype=tf.float32)
    
    outputs = tf.keras.layers.Dropout(0.2)(outputs)
    # 4번째 설명
    hidden_layer = tf.keras.layers.Dense(DENSE_FEATURE_DIM, activation=tf.nn.tanh)(outputs[:,-1,:])
    hidden_layer = tf.keras.layers.Dropout(0.2)(hidden_layer)
    # 5번째 설명
    logits = tf.keras.layers.Dense(1)(hidden_layer)
    logits = tf.squeeze(logits, axis=-1)
    
    sigmoid_logits = tf.nn.sigmoid(logits)
    
    if PREDICT:
        predictions = {'sentiment': sigmoid_logits}
        
        return tf.estimator.EstimatorSpec(
                  mode=mode,
                  predictions=predictions)
    
    # 모델 학습을 위한 부분
    loss = tf.losses.sigmoid_cross_entropy(labels, logits)
    
    if EVAL:
        accuracy = tf.metrics.accuracy(labels, tf.round(sigmoid_logits))
        eval_metric_ops = {'acc': accuracy}

        return tf.estimator.EstimatorSpec(mode, loss=loss, eval_metric_ops=eval_metric_ops)
    
    if TRAIN:
        global_step = tf.train.get_global_step()
        train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss, global_step)

        return tf.estimator.EstimatorSpec(
                  mode=mode,
                  train_op=train_op,
                  loss=loss)

이 코드는 모델 함수 전체를 구현한 것이며, 구체적으로는 다음과 같다.

 

# 1번째 설명

embedding_layer = tf.keras.layers.Embedding(
                    VOCAB_SIZE,
                    WORD_EMBEDDING_DIM)(features['x'])

모델에서 배치 데이터를 받게 된다면 단어 인덱스로 구성된 시퀀스 형태로 입력이 들어온다.

앞서 정의했듯이 입력 인자는 딕셔너리 형태로 이루어져 있다.

모델에 들어온 입력 인자는 임베딩 층을 거치는데, 이 역할을 tf.keras.layers.Embedding함수가 해준다.

 

# 2번째 설명

rnn_layers = [tf.nn.rnn_cell.LSTMCell(size) for size in [HIDDEN_STATE_DIM, HIDDEN_STATE_DIM]]
multi_rnn_cell = tf.nn.rnn_cell.MultiRNNCell(rnn_layers)

순환 신경망을 구현한 것이며 이 부분을 거쳐 문장의 의미 벡터를 출력한다.

이 모델은 LSTM모델을 통해 구현하였으며, LSTM으로 순환 긴경망을 구현하기 위해 텐서플로의 tf.nn.rnn_cell.LSTMCell 객체를 생성한다.

이 객체는 하나의 LSTM Cell을 의미하기 때문에 Cell 객체를 여러개 생성해서 하나의 리스트로 만든다.

LSTMCell을 생성할 때는 은닉 상태 벡터(hidden state vector)에 대한 차원만 정의하면 된다.

이렇게 여러 LSTMCell을 쌓게 되면 이를 하나의 MultiRNN으로 래핑(wrapping)해야 한다.

그러므로 tf.nn.rnn_cell.MultiRNNCell을 생성하여 스택 구조의 LSTM 신경망을 구현한다.

 

# 3번째 설명

 outputs, state = tf.nn.dynamic_rnn(cell=multi_rnn_cell,
                                       inputs=embedding_layer,
                                       dtype=tf.float32)

단순히 for문을 활용해 모델 연산 그래프를 만들 수 있지만 tf.nn.dynamic_rnn함수를 사용하여 훨씬 더 간단하게 구현할 수 있다.

이 함수는 for문 없이 자동으로 순환 신경망을 만들어 주는 역할을 한다.

dynamic_rnn함수에 필요한 입력 인자는 2개인데, 첫번째는 순환 신경망 객체이며 두번째는 입력값을 넣어준다.

마지막으로 dtype인자를 통해 출력값의 데이터 타입을 설정해줄 수 있다.

 

# 4번째 설명

hidden_layer = tf.keras.layers.Dense(DENSE_FEATURE_DIM, activation=tf.nn.tanh)(outputs[:,-1,:])

앞에서 나온 출력값에 Dense를 적용시키는 부분이다. 

출력값에 [:, -1, :]로 마지막 값만 뽑아낸 후 Dense에 적용시킨다.

그리고 활성화 함수로 하이퍼블릭 탄젠트 함수를 사용한다.

 

# 5번째 설명

logits = tf.keras.layers.Dense(1)(hidden_layer)

마지막으로 감정이 긍정인지 부정인지 판단 가능하도록 출력값을 하나로 만들어야 하는데, 보통 선형 변환을 통해 차원 수를 바꾼다. 여기서는 tf.keras.layers.Dense 함수가 이와 같은 역할을 한다.

앞서 은닉층을 구현한 것과 동일하게 Dense 객체를 생성하고 호출 시 units 인자에 1을 입력한다.

이렇게 구현을 마치면 감정 분류 모델이 된다.

 

이제 모델을 학습하기 위한 구현 부분이다.

loss = tf.losses.sigmoid_cross_entropy(labels, logits)
if PREDICT:
	predictions = {'sentiment': sigmoid_logits}
	return tf.estimator.EstimatorSpec(
    	mode=mode,
    	predictions=predictions)    
    
if EVAL:
    accuracy = tf.metrics.accuracy(labels, tf.round(sigmoid_logits))
    eval_metric_ops = {'acc': accuracy}

    return tf.estimator.EstimatorSpec(mode, loss=loss, eval_metric_ops=eval_metric_ops)
    
if TRAIN:
    global_step = tf.train.get_global_step()
    train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss, global_step)

    return tf.estimator.EstimatorSpec(
              mode=mode,
              train_op=train_op,
              loss=loss)

여기서 모델 예측 loss 값은 모델에서 구한 logits 변수와 정답인 labels 변수를 가지고 구한다.

이 때 logits 변수의 경우 아직 로지스틱(logistic)함수를 통해 0~1 사이의 값으로 스케일을 맞춰야 한다.

앞서 dense층에서 activation인자를 tf.nn.sigmoid로 설정해둘 수 있지만, tf.losses.sigmoid_cross_entropy함수를 활용해 손실 값을 구할 수 있기 때문에 dense층에서 설정하지 않았다.

 

이렇게 예측 손실값을 구하고 나면 이제 파라미터 최적화를 위해 경사도 하강법을 진행한다.

이번에는 경사도 하강법 중 하나인 아담 옵티마이저를 활용하는데, tf.train.AdamOptimizer클래스를 활용하면 된다.

AdamOptimizer객체를 생성할 때 생성자 입력 인자로 학습률 값을 입력한다.

또한 tf.train.AdamOptimizer.minimize 함수를 선언할 시 전체 학습에 대한 글로벌 스텝(global step)값을 넣어야 한다.

텐서플로에서는 tf.train.get_global_step을 선언하면 현재 글로벌 스텝을 얻을 수 있다.

 

마지막으로 return 부분을 살펴보자.

보통 직접 모델 함수를 구현하게 되면 tf.estimator.EstimatorSpec객체를 생성해서 반환한다.

이 객체는 현재 함수가 어느 모드에서 실행되고 있는지 확인하고 각 모드에 따라 필요한 입력 인자를 받는다.

학습하는 경우라면 학습 연산과 손실 값이 필요하기 때문에 train_op와 loss변수를 입력한다.

평가하는 경우에는 eval_metric_ops를 인자로 입력한다.

예측하는 경우라면 모델 출력값에 대해 tf.nn.sigmoid함수를 활용해 0~1 사이의 값으로 정의한 후 결괏값을 dict객체로 표현해 에스티메이터에서 반환할 예측값을 만든다.

출력값을 prediction에 입력하여 예측값을 반환한다.

 

이제 구현한 모델을 통해 에스티메이터 객체를 생성해보자.

if not os.path.exists(DATA_OUT_PATH):
    os.makedirs(DATA_OUT_PATH)

est = tf.estimator.Estimator(model_fn=model_fn,
                             model_dir=DATA_OUT_PATH + 'checkpoint/rnn')
                             
# 기본 형식
# tf.estimator.Estimator(model_fn, model_dir=None, config=None, params=None, warm_start_from=None)

tf.estimator.Estimator 객체를 생성 후 필요한 인자를 입력한다.

tf.estimator.Estimator의 인자들의 설명은 다음과 같다.

  • model_fn : 모델 함수 이름 입력
  • model_dir : 모든 입력과 출력을 기록하려는 디렉토리 입력(설정되지 않은 경우 임시 디렉토리 사용)
  • config : 구성 개체
  • params : dict 형태의 하이퍼 파라미터
  • warm_start_from : warm_start할 체크포인트 또는 저장된 모델에 대한 선택적 문자열 파일 경로/개체

이를 통해 객체를 생성하고 학습, 평가, 예측 등의 실행이 가능해졌다.

 

이제 모델 학습을 해보자.

est.train(train_input_fn)

앞서 생성한 에스티메이터 객체인 est에 대해 train 함수를 호출하면 간단하게 모델 학습이 진행된다.

주어진 입력 데이터 함수에서 정의한 데이터에 대해 학습을 진행하는데, 학습하는 횟수를 1 에폭(epoch)이라고 한다.

매번 함수를 호출할 때마다 모델은 한 에폭에 대한 학습을 진행한다.

이렇게 학습을 진행한 결과의 대한 loss값을 다음과 같이 확인할 수 있다.

결과 :

학습이 완료되었다면 모델을 평가해보자.

est.evaluate(eval_input_fn)

결과 :

{'acc': 0.5, 'loss': 0.6929564, 'global_step': 4221}

 

evaluate함수는 현재 학습한 모델에 대해 평가 데이터셋을 활용하여 모델 성능이 어느정도인지 확인할 수 있게 해준다.

 

728x90
반응형
LIST