합성곱 신경망(CNN)이란, 딥러닝 부흥을 이끈 모델 중 하나로 전통적인 신경망 앞에 여러 계층의 합성곱(convolution) 계층을 쌓은 모델로, 입력받은 이미지에 대한 가장 좋은 특징을 만들어 내도록 학습하고, 추출된 특징을 활용해 이미지를 분류하는 방식.
일반적으로 이미지는 이미지 파일 각각 강아지, 고양이, 말 등의 라벨을 붙여 데이터셋을 만들고, 모델이 학습하면서 각 특징값을 추출해서 특징을 배우고, 가장 가까운 라벨을 예측한다.
그렇다면 텍스트에서는 합성곱 신경망을 어떻게 활용할까?
< 사진 출처 : arxiv.org/abs/1510.03820 >
RNN이 단어 입력 순서를 중요시 했다면 CNN은 문장의 지역 정보를 보존하면서 각 문장 성분의 등장 정보를 학습에 반영하는 구조로 풀어나간다. 학습할 때 각 필터 크기를 조절하며 언어의 특징 값을 추출하는데 기존의 n-gram(2그램, 3그램) 방식과 유사하다고 볼 수 있다.
예를 들어, "나는 배가 고프다" 에서 2그램을 사용하면 "나 는 / 는 배/ 배 가 / 가 고프 / 고프 다/"로 각각 문장의 단어 성분을 쪼개서 활용하는 접근법을, 단어의 각 벡터 값을 투영해서 컨볼루션 필터값에 적용하는 원리다. 위 그림에서는 "I like this movie very much!"가 주어질 때 느낌표를 포함한 총 7개의 단어와 각 5차원(d=)를 보유하고 있다. 이후에 2,3,4 개의 단어 필터를 추출해서 피처 맵을 생성하고 맥스 풀링을 수행한 후 각 값이 무엇을 의미하는지 추출한다.
코드 구조는 기존의 RNN에서 설명한 에스티메이터 구조를 그대로 차용했으며, 모델 쪽 코드만 변경하면 쉽게 CNN으로 적용할 수 있다. 모델의 전후 파이프라인만 구성하면 손쉽게 모델 구조를 바꿔서 평가할 수 있다. 전처리 및 데이터를 불러오는 구조는 같고, 모델 부분에서 이전 모델과 달라진 부분에 대해 중심적으로 설명한다.
# 기본적인 라이브러리 호출
import sys
import os
import numpy as np
import json
import tensorflow.compat.v1 as tf
from tensorflow.compat.v1 import keras
# 이전에 저장했던 학습에 필요한 디렉터리 설정 및 학습/평가 데이터를 불러온다.
DATA_IN_PATH='./data_in/'
DATA_OUT_PATH='./data_out/'
INPUT_TRAIN_DATA_FILE_NAME='train_input.npy'
LABEL_TRAIN_DATA_FILE_NAME='train_label.npy'
TEST_INPUT_DATA_FILE_NAME='test_input.npy'
DATA_CONFIGS_FILE_NAME='data_configs.json'
train_input_data=np.load(open(DATA_IN_PATH+INPUT_TRAIN_DATA_FILE_NAME,'rb'))
train_label_data=np.load(open(DATA_IN_PATH+LABEL_TRAIN_DATA_FILE_NAME,'rb'))
test_input_data=np.load(open(DATA_IN_PATH+TEST_INPUT_DATA_FILE_NAME,'rb'))
with open(DATA_IN_PATH+DATA_CONFIGS_FILE_NAME,'r') as f:
prepro_configs=json.load(f)
print(prepro_configs.keys())
from sklearn.model_selection import train_test_split
# 파라미터 변수
RNG_SEED=1234
BATCH_SIZE=128
NUM_EPOCHS=10000
VOCAB_SIZE=prepro_configs['vocab_size']
EMB_SIZE=128
VALID_SPLIT=0.2
# 학습 데이터와 검증 데이터를 train_test_split 함수를 활용해 나눈다.
train_input, eval_input, train_label, eval_label=train_test_split(train_input_data, train_label_data, test_size=VALID_SPLIT, random_state=RNG_SEED)
# 전처리 학습을 위해 tf.data를 설정한다.
def mapping_fn(X, Y=None):
input, label={'x':X},Y
return input,label
def train_input_fn():
dataset=tf.data.Dataset.from_tensor_slices((input_train, label_train))
dataset=dataset.shuffle(buffer_size=len(input_train))
dataset=dataset.batch(BATCH_SIZE)
dataset=dataset.map(mapping_fn)
dataset=dataset.repeat(count=NUM_EPOCHS)
iterator=dataset.make_one_shot_iterator()
return iterator.get_next()
def eval_input_fn():
dataset=tf.data.Dataset.from_tensor_slices((input_eval,label_eval))
dataset=dataset.shuffle(buffer_size=len(input_eval))
dataset=dataset.batch(BATCH_SIZE)
dataset=dataset.map(mapping_fn)
iterator=dataset.make_one_shot_iterator()
return iterator.get_next()
모델 자체도 대부분 이전 재현 신경망 모델과 거의 유사하다. 합성곱 연산의 경우 케라스 모듈 중 Conv1D를 활용해 진행한다. 총 3개의 합성곱 층을 사용하는데, 각각 필터의 크기를 다르게 해서 적용한다. 즉, kernel_size=3,4,5.
이렇게 다른 필터 크기로 적용한 합성곱 층 출력값을 하나로 합친다.
그리고 추가로 각 합성곱 신경망 이후에 맥스 풀링 층을 적용. 즉, 모델은 총 3개의 합성곱+맥스 풀링층을 사용하는 구조다. 모델 함수를 보면,
def model_fn(features, labels, mode):
TRAIN=mode==tf.estimator.ModeKeys.TRAIN
EVAL=mode==tf.estimator.ModeKeys.EVAL
PREDICT=mode==tf.estimator.ModeKeys.PREDICT
# embedding layer를 선언합니다.
embedding_layer=keras.layers.Embedding(VOCAB_SIZE,EMB_SIZE)(features['x'])
# embedding layer에 대한 output에 대해 dropout을 취합니다.
dropout_emb=keras.layers.Dropout(rate=0.5)(embedding_layer)
## filters=128이고 kernel_size=3,4,5다.
## 길이가 3, 4, 5인 128개의 다른 필터를 생성한다. 3,4,5 gram의 효과처럼 다양한 각도에서 문장을 보는 효과가 있다.
## conv1d는 (배치 크기, 길이, 채널)로 입력값을 받는데, 배치 사이즈: 문장 숫자 | 길이: 각 문장의 단어의 개수 | 채널 : 임베딩 출력 차원수임
conv1=keras.layers.Conv1D(filters=128, kernel_size=3, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool1=keras.layers.GlobalMaxPool1D()(conv1)
conv2=keras.layers.Conv1D(filters=128, kernel_size=4, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool2=keras.layers.GlobalMaxPool1D()(conv2)
conv3=keras.layers.Conv1D(filters=128, kernel_size=5, padding='valid', activation=tf.nn.relu)(dropout_emb)
pool3=keras.layers.GlobalMaxPool1D()(conv3)
# 3,4,5gram 이후 모아주기
concat=keras.layers.concatenate([pool1, pool2, pool3])
hidden=keras.layers.Dense(250, activation=tf.nn.relu)(concat)
dropout_hidden=keras.layers.Dropout(rate=0.5)(hidden)
logits=keras.layers.Dense(1, name='logits')(dropout_hidden)
logits=tf.squeeze(logits, axis=-1)
# 최종적으로 학습, 검증, 평가의 단계로 나누어 활용
if PREDICT:
return tf.estimator.EstimatorSpec(mode=mode, predictions={'prob':tf.nn.sigmoid(logits)})
loss=tf.losses.sigmoid_cross_entropy(labels, logits)
if EVAL:
pred=tf.nn.sigmoid(logits)
accuracy=tf.metrics.accuracy(labels, tf.round(pred))
return tf.estimator.EstimatorSpec(mode=mode, loss=loss, eval_metric_ops={'acc':accuracy})
if TRAIN:
global_step=tf.train.get_global_step()
train_op=tf.train.AdamOptimizer(0.001).minimize(loss, global_step)
return tf.estimator.EstimatorSpec(mode=mode, train_op=train_op, loss=loss)
이렇게 구현한 모델 함수를 적용한 에스티메이터 객체를 생성해야 한다. 앞에서 한 것과 동일하게 체크포인트를 저장할 경로와 모델 함수를 적용해 에스티메이터 객체를 생성한 후 모델을 학습 및 검증한다.
model_dir=os.path.join(os.getcwd(),"data_out/checkpoint/cnn/")
os.makedirs(model_dir, exist_ok=True)
# Estimator 객체 생성
cnn_est=tf.estimator.Estimator(model_fn, model_dir=model_dir)
# 학습하기
cnn_est.train(train_input_fn)
# 평가하기
enn_est.evaluate(eval_input_fn)
학습이 시작되면 지속적으로 최적화하면서 최적의 손실값을 찾아낸다.
근데 학습이 2시간이 되도록 안 끝났다. 분명 책이랑 코드가 같았는데, 그래서 책에서 제공한 코드를 봤더니
NUM_EPOCHS 값이 3으로 되어있었다... ㅎㅎ 책은 10000이였는데....
# 위에서 NUM_EPOCHS 값을 찾아 3으로 바꿔준다.
NUM_EPOCHS=3
무튼 학습이 끝났다.
이제 캐글에 제출하기 위한 평가 csv 파일을 활용해 지금까지 학습시킨 모델로 제출파일을 만든다. 우선 평가 데이터와 제출 형식을 맞추기 위해 전처리 과정에서 저장한 평가 데이터의 id값을 불러온다.
import pandas as pd
DATA_IN_PATH='./data_in/'
DATA_OUT_PATH='./data_out/'
TEST_INPUT_DATA='test_input.npy'
TEST_ID_DATA='test_id.npy'
test_input_data=np.load(open(DATA_IN_PATH+TEST_INPUT_DATA,'rb'))
ids=np.load(open(DATA_IN_PATH+TEST_ID_DATA,'rb'), allow_pickle=True)
평가함수의 경우 별도의 입력 함수를 안 만들었는데, 간단히 에스티메이터의 numpy_input_fn을 쓴다. 그리고 입력 함수를 정의한 뒤 에스티메이터에 적용해서 나온 예측 결과와 평가 데이터의 id값을 하나의 데이터프레임으로 합친 후 csv파일로 저장한다. 그리고 제출함.
predict_input_fn=tf.estimator.inputs.numpy_input_fn(x={"x":test_input_data}, shuffle=False)
predictions=np.array([p['prob'] for p in cnn_est.predict(input_fn=predict_input_fn)])
output=pd.DataFrame(data={"id":list(ids), "sentiment":list(predictions)})
output.to_csv(DATA_OUT_PATH+"Bag_Words_model_test_cnn.csv",index=False, quoting=3)
생각 이상으로 높은 점수가 나왔다.
기본적으로 데이터의 수가 일정 이상 되는 경우 보통 딥러닝 모델의 성과가 좋다고 알려져있다. 지금까지 했던 것은 기본적인 사용법에 불과하며 좀 더 심화된 방법이나 여러 기법을 섞는다면 더욱 높은 성능 향상을 기대할 수 있다고 한다.
[ 출처 : 책 ( 텐서플로와 머신러닝으로 시작하는 자연어 처리) ]
[ NLP ] 한글 텍스트 분류 (2) (0) | 2021.01.13 |
---|---|
[ NLP ] 한글 텍스트 분류 (1) (0) | 2021.01.12 |
[ NLP ] 영어 텍스트 분류 (6) (0) | 2020.12.30 |
[ NLP ] 영어 텍스트 분류 (5) (0) | 2020.12.22 |
[ NLP ] 영어 텍스트 분류 (4) (0) | 2020.12.14 |