CNN을 이용한 한국어 문장 분류

문장을 분류하는 기술은 기존 머신 러닝의 네이브 베이즈(Naive Bayes) 및 서포트 벡터 머신(Support Vector Machine)이 널리 쓰여왔다. 특히 서포트 벡터 머신은 비교적 간편한 방법으로 고성능의 분류를 수행할 수 있어 딥러닝 이전까지 널리 쓰였다. 여기서는 서포트 벡터 머신의 성능을 뛰어 넘는 딥러닝 알고리즘인 합성곱 신경망(Convolutional Neural Networks / CNN / ConvNets)을 이용하여 한국어 문장 분류를 진행하고 과정을 정리하여 소개해 보도록 한다.

  • 소개

  • 내용

    • 전처리(NLP Preprocessing)

    • 데이타

    • 모델

    • 구현

      • Embedding Layer

      • Convolution and Max-Pooling Layers

      • Dropout Layer

      • Scores and Predictions

      • Loss and Accuracy

      • Visualizing the Network

      • Instantiating the CNN and Minimizing the Loss

      • Summaries

      • Initializing the Variables

      • Defining a Single Training Step

      • Training Loop

      • Visualizing Results in Tensorboard

      • Extensions and Exercises

  • 코드

  • References

소개

CNN 알고리즘은 주로 이미지의 특징을 추출하여 유사점을 찾는 이미지 판단에 사용되었다. 합성곱(Convolution)이 핵심인데 매트릭스에 적용하는 슬라이딩 윈도우 함수를 생각하면 이해하기 쉽다.

 

왼쪽 매트릭스가 흑백 이미지를 표현한다면 각 엔트리는 1 픽셀의 0 검정, 1 흰색으로 간주할 수 있고 슬라이딩 윈도우는 3×3 필터를 사용하여 매트릭스의 원소 값을 곱셈하여 합산한다. 그 결과 전체 매트릭스에 필터를 슬라이딩 하여 각 요소의 전체 합성곱을 얻었다.

 

 

이를 겹겹이 쌓아올려 신경망을 구성하는데 위와 같이 이미지의 필터링된 합성곱(CONV), 활성화 함수(ReLU), 최대 풀링(POOL) 과정을 반복하여 특징 벡터를 형성하고 학습된 이미지와 비교하여 유사도를 판별한다. Andrej Karpathy가 순수 JS로 구현한 CIFAR-10 시각화 데모는 각각의 출력이 어떻게 형성되는지 이해하는데 도움이 된다.

또한 CNN 알고리즘을 NLP에 적용하려는 노력이 있었고 의미 있는 결과를 낸 논문들이 다수 출간됐다.

 

 

위 이미지는 A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification 논문에 등장한 문장 분류를 위한 합성곱 신경망(CNN) 아키텍처다. 초기에 단어를 벡터로 임베딩하는 과정이 진행되어야 함을 제외하면 이미지 분류와 크게 다르지 않다. 3개의 필터 사이즈 2,3,4를 각 두 개씩 총 6개를 문장 매트릭스에 합성곱을 수행하고 특징 맵을 생성한다. 이후 각 맵에 대해 최대 풀링을 진행하여 각 특징 맵으로부터 가장 큰 수를 남긴다. 이들 6개 맵에서 단변량 특징 벡터가 생성되고 이들 6개 특징은 두 번째 레이어를 위한 특징 벡터로 연결한다. 최종적으로 소프트맥스 레이어는 특징 값을 받아 문장을 분류한다. 여기서는 이진 분류를 가정했고 따라서 두 가지 가능한 출력 상태를 묘사했다.1

아울러 김윤 박사가 문장 분류에 CNN을 적용한 논문을 발표했고 좋은 결과를 보였다. 이를 참고하여 구현한 문서가 IMPLEMENTING A CNN FOR TEXT CLASSIFICATION IN TENSORFLOW이며 여기서는 ‘원문’으로 표기하며 본 문서에 소개하는 대부분의 내용과 코드는 원문을 참조하여 작성했음을 미리 밝혀둔다.

내용

개념과 원리만 소개하는 여타 논문과 달리 원문에서는 텐서플로우를 이용해 직접 실행 가능한 코드를 제공한다. 원본 코드는 깃헙에 있으며 이를 포크(fork)하여 몇몇 부분은 직접 개선했다. 특히 본 문서에서는 영어가 아닌 한국어 문장 분류를 위해 패치한 버전을 활용하도록 한다. 아울러 원문에는 긍정/부정의 이중 분류만 가능하도록 되어 있으나 마침 makinada라는 사용자가 다중 분류가 가능하도록 PR을 제공했고 본 문서를 작성하는 현 시점에 아직 업스트림에 머지 되진 않았으나 개선 버전에는 미리 적용하여 약간의 수정을 거쳐 실제 사용해 보기로 한다.

전처리(NLP Preprocessing)

먼저 입력값은 형태소 분석기를 통해 전처리된 어휘를 사용 한다. 원본 코드는 클린징 처리만 하여 그대로 사용하는데 영어를 기준으로 작성되어 있어 한글의 경우 이 코드는 모든 한글을 날려버리므로 사용할 수 없다. 또한 한글은 조사등의 복잡한 언어 구조상 NLP 전처리 작업이 효율적이기 때문에 미리 처리하도록 한다. 한글로 작성된 아래 논문에도 NLP 전처리를 미리 진행하는 형태로 모식도가 설계되어 있음을 확인할 수 있다.

 

 

NLP 전처리를 위해서는 한글 형태소 분석기가 필수적인데 여기서는 카카오 사내 고성능 형태소 분석기인 DHA를 사용했다. 우수한 형태소 분석기이지만 아쉽게도 DHA는 사내 솔루션이며 외부에 공개되지 않는다. 따라서 더 이상 소개할 수 없는 점 양해 바란다. 대신 KoNLPy등의 오픈소스 형태소 분석기를 활용할 수 있을 것이다.

데이타

한국어로 미리 DHA 전처리 결과 100 문장을 학습 데이타로 제공했다. 평가 데이타는 10개 문장을 제공하여 10% 비율로 구성했다. 당연히 실제 서비스를 하려면 이보다 훨씬 더 풍부한 데이타가 필요하다. 앞서 서울대 논문을 보면 대용량 분류를 위해 62만여개의 문장을 학습했다고 하며 결과가 더욱 정확하고 정교해지려면 학습 데이타는 당연히 많을수록 좋다.

 

 

딥러닝에서 학습 데이타는 많을수록 좋다고 하는 앤드류 응의 슬라이드 참고. 또한 원문의 알고리즘은 단어 사전에 없을 경우 동일한 벡터값으로 표현되기 때문에 정확도가 떨어진다. 따라서 이를 높이기 위해서는 모든 문장의 단어를 커버할 수 있을 정도로 학습하는게 좋다.

문장 데이타는 어휘로 구분하여 색인하고 0에서 전체 어휘 사이즈 만큼 맵핑한다. 각 문장은 정수 벡터가 된다.

모델

구현하려는 신경망을 러프하게 구성하면 아래와 같다.

 

 

첫 번째 레이어는 단어를 저차원 벡터로 임베드한다. 그 다음 레이어는 여러 사이즈의 필터를 이용하여 임베디드 단어 벡터에 대해 합성곱(convolutions) 변환을 수행한다. 그 다음 합성곱 레이어의 결과를 긴 특징 벡터로 최대 풀링하고, 드롭아웃 정규화를 추가하고, 소프트맥스 레이어를 통한 결과로 분류를 수행한다.

이 글에서 참고한 김윤 박사의 논문에는 word2vec 벡터를 사용했으나 원문에서는 교육 목적등으로 모델을 보다 단순화 하기로 한다. 참고로 서울대 논문에 따르면 word2vec으로 성능 향상에 큰 도움은 되지 않았다고 한다.

구현

출간된 여러 논문을 살펴 보면 개념과 원리는 상세하게 소개하지만 어떻게 구현했는지는 언급하지 않는다. 사용한 프레임워크는 물론 단 한 줄의 코드도 제공하지 않기 때문에 동일한 환경을 구성하여 실험하기에는 어려움이 있다. 그런데 원문에서는 텐서플로우를 이용해 논문의 모델을 실제로 구현하는 방법을 소개하여 매우 유용하다. 실제 원문의 내용 대부분은 구현을 비롯한 코드 소개에 할애하고 있고 본 문서에서도 구현에 대해 보다 상세히 정리하여 소개하도록 한다. 본 문서는 원문의 많은 부분을 인용하였으므로 별도 인용 문구를 표시하진 않으나 대부분의 내용은 거의 유사함을 다시 한 번 일러둔다.

Embedding Layer

첫 번째 레이어는 단어 색인을 저차원 벡터 표현로 매핑하는 임베디드 레이어로 필수적인 룩업 테이블이다. 앞서 김윤 박사의 논문3에는 word2vec으로 표현되어 있으나 여기서는 단순 룩업 테이블을 사용한다.

Convolution and Max-Pooling Layers

이 부분이 가장 중요한 최대 풀링(max-pooling) 합성곱(convolution) 레이어를 만드는 부분이다. 각각 크기가 다른 필터를 사용하여 반복적으로 합성곱 텐서를 생성하고 이를 하나의 큰 특징 벡터로 병합한다.

pooled_outputs = []
for i, filter_size in enumerate(filter_sizes):
with tf.name_scope(“conv-maxpool-%s” % filter_size):
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filters]
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name=”W”)
b = tf.Variable(tf.constant(0.1, shape=[num_filters]), name=”b”)
conv = tf.nn.conv2d(
self.embedded_chars_expanded,
W,
strides=[1, 1, 1, 1],
padding=”VALID”,
name=”conv”)
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name=”relu”)
# Max-pooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length – filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding=’VALID’,
name=”pool”)
pooled_outputs.append(pooled)

# Combine all the pooled features
num_filters_total = num_filters * len(filter_sizes)
self.h_pool = tf.concat(3, pooled_outputs)
self.h_pool_flat = tf.reshape(self.h_pool, [-1, num_filters_total])

여기서, W는 필터 행렬이고, h는 합성곱 출력에 비선형성(ReLU 활성화)을 적용한 결과다. 각 필터는 전체 임베딩을 슬라이드 하지만 커버하는 단어 수에 따라 다르다. VALID 패딩은 엣지 패딩 없이 문장을 슬라이드 하여 [1, sequence_length – filter_size + 1, 1, 1] 모양의 좁은 합성곱을 수행함을 뜻한다. 각 필터 사이즈의 최대 풀링(max-pooling) 출력은 [batch_size, 1, 1, num_filters]가 되며 이것이 최종 특징에 대응하는 마지막 특징 벡터다. 모든 풀링 벡터는 [batch_size, num_filters_total] 모양을 갖는 하나의 긴 특징 벡터로 결합된다. tf.reshape에 -1을 사용하여 가능한 텐서플로우가 차원을 평평하게 만들도록 한다.

설명이 다소 어렵지만 시간을 내서 각각의 작업에 대한 출력 모양을 가능하면 시각화하고 이해하려고 노력할 필요가 있다. 이미지 CNN에 대한 시각화 자료는 대체로 많은 편이고 특히 Andrej Karpathy가 순수 JS로 구현한 CIFAR-10 시각화 데모는 각각의 출력을 이해하는데 큰 도움이 된다.

Dropout Layer

드롭아웃은 합성곱 신경망의 오버피팅을 방지하는 가장 유명하면서도 가장 흥미로운 방법이다. 드롭아웃 레이어는 뉴런의 일부를 확률적으로 ‘비활성화’한다. 이는 뉴런의 동시 적응을 방지하고 특징을 개별적으로 학습하도록 강제하여 사람이 그림을 맞출때 일부를 손으로 가린채 특징을 학습하여 맞추도록 하는 방식과 유사하다. 여기서는 드롭아웃을 학습 중에는 0.5(즉, 50%), 평가 중에는 1(당연히 비활성화)로 설정한다.

Scores and Predictions

최대 풀링(드롭아웃이 적용된 상태에서)으로 특징 벡터를 사용하여 행렬 곱셈을 수행하고 가장 높은 점수로 분류를 선택하는 예측을 수행한다. 원 점수를 정규화 확률로 변환하는 소프트맥스를 적용하지만 최종 예측 결과는 변하지 않는다.

with tf.name_scope(“output”):
W = tf.Variable(tf.truncated_normal([num_filters_total, num_classes], stddev=0.1), name=”W”)
b = tf.Variable(tf.constant(0.1, shape=[num_classes]), name=”b”)
self.scores = tf.nn.xw_plus_b(self.h_drop, W, b, name=”scores”)
self.predictions = tf.argmax(self.scores, 1, name=”predictions”)

Loss and Accuracy

점수를 이용해 손실 함수를 정의한다. 손실은 망에서 발생하는 오류를 나타내는 척도이며 이를 최소화 하는게 우리의 목표다. 분류 문제에 대한 표준 손실 함수는 cross-entropy loss를 사용한다.

# Calculate mean cross-entropy loss
with tf.name_scope(“loss”):
losses = tf.nn.softmax_cross_entropy_with_logits(self.scores, self.input_y)
self.loss = tf.reduce_mean(losses)

Visualizing the Network

딥러닝은 레이어가 매우 복잡해질 수 있기 때문에 시각화가 중요하다. 텐서플로우에는 텐서보드라는 훌륭한 시각화 도구가 포함되어 있고 이를 이용해 시각화 할 수 있다. 자주 활용하여 각각의 레이어 입출력을 명확히 이해하는게 중요하다.

 

 

Instantiating the CNN and Minimizing the Loss

TextCNN 모델을 인스턴스화한 다음 망의 손실 함수를 최적화하는 방법을 정의한다. 텐서플로우에는 여러가지 옵티마이저가 내장되어 있는데 여기서는 아담옵티마이저를 사용한다.

global_step = tf.Variable(0, name=”global_step”, trainable=False)
optimizer = tf.train.AdamOptimizer(1e-4)
grads_and_vars = optimizer.compute_gradients(cnn.loss)
train_op = optimizer.apply_gradients(grads_and_vars, global_step=global_step)

Summaries

텐서플로우에는 다양한 학습 및 평가 과정을 추적하고 시각화 하는 써머리(Summaries) 개념이 있다. 예를 들어 시간 경과에 따른 손실 및 정확도의 변화를 추적하고 싶을때 레이어 활성화 히스토그램 같은 더 복잡한 부분도 추적할 수 있다. 써머리는 직렬화 된 오브젝트이며 써머리라이터(SummaryWriter)를 사용해 디스크에 기록한다.

# Output directory for models and summaries
timestamp = str(int(time.time()))
out_dir = os.path.abspath(os.path.join(os.path.curdir, “runs”, timestamp))
print(“Writing to {}\n”.format(out_dir))

# Summaries for loss and accuracy
loss_summary = tf.scalar_summary(“loss”, cnn.loss)
acc_summary = tf.scalar_summary(“accuracy”, cnn.accuracy)

# Train Summaries
train_summary_op = tf.merge_summary([loss_summary, acc_summary])
train_summary_dir = os.path.join(out_dir, “summaries”, “train”)
train_summary_writer = tf.train.SummaryWriter(train_summary_dir, sess.graph_def)

# Dev summaries
dev_summary_op = tf.merge_summary([loss_summary, acc_summary])
dev_summary_dir = os.path.join(out_dir, “summaries”, “dev”)
dev_summary_writer = tf.train.SummaryWriter(dev_summary_dir, sess.graph_def)

Initializing the Variables

모델을 학습하기 전에 그래프에서 변수를 초기화 해야 한다. initialize_all_variables 함수를 사용했으며 정의한 변수를 모두 초기화 하는 편리한 함수다. 현재는 Deprecated 된 상태이므로 이에 대한 간단한 패치를 제출했고 머지 되었다. 물론 변수의 초기화 프로그램을 수동으로 호출 할 수도 있는데 미리 훈련 된 값으로 임베딩을 초기화하려는 경우에 유용하다.

Defining a Single Training Step

이제 데이타 배치 모델을 평가하고 모델 파라미터를 업데이트하는 학습 단계를 정의한다.

def train_step(x_batch, y_batch):
“””
A single training step
“””
feed_dict = {
cnn.input_x: x_batch,
cnn.input_y: y_batch,
cnn.dropout_keep_prob: FLAGS.dropout_keep_prob
}
_, step, summaries, loss, accuracy = sess.run(
[train_op, global_step, train_summary_op, cnn.loss, cnn.accuracy],
feed_dict)
time_str = datetime.datetime.now().isoformat()
print(“{}: step {}, loss {:g}, acc {:g}”.format(time_str, step, loss, accuracy))
train_summary_writer.add_summary(summaries, step)

평가 단계도 학습 단계와 유사하게 정의할 수 있다. 학습의 유효성을 검증하기 위해 평가 셋트의 손실 및 정확도 평가를 위한 유사한 기능을 작성하며, 차이점은 별도 학습 과정이 필요 없으며 드롭아웃을 비활성화 한다는 점 뿐이다.

Training Loop

마지막으로 트레이닝 루프를 작성한다. 데이터 배치를 반복하고 주기적으로 모델을 평가하고 체크포인팅 한다. 기본 값은 100회당 한 차례 모델을 평가하도록 설정 되어 있으며 파라미터로 이 값을 조정할 수 있다.

Visualizing Results in Tensorboard

앞서 출력 디렉토리에 써머리를 저장하였는데, 텐서보드에 이 위치를 지정하여 그래프와 요약 정보를 시각화 할 수 있다. 앞서 언급했듯 시각화는 매우 중요하며 반드시 필요하다.

tensorboard –logdir /PATH_TO_CODE/runs/1449760558/summaries/

예제를 통한 학습 결과의 시각화는 아래와 같다. 한국어 학습 데이타는 규모가 작으므로 여기서는 참고를 위해 원문의 학습 그래프를 소개하도록 한다.

기본 파라미터(128 차원 임베딩, 3,4,5 필터 사이즈, 드롭아웃 0.5, 필터 사이즈별 128개 필터)의 학습 결과(파란선이 학습 데이타, 빨간선은 평가 데이타)이며 아래와 같다.

 

 

  • 배치 사이즈가 작기 때문에 학습 결과가 부드럽지 않다. 더 큰 배치를 사용하거나 전체 학습 결과로 평가한다면 좀 더 부드러운 파란색 선을 얻을 수 있을 것이다.

  • 평가 정확도가 학습 정확도보다 상당히 낮기 때문에 우리 망이 학습 데이타를 오버피팅 하는 것 처럼 보인다. 따라서 더 많은 데이타, 더 강력한 정규화 또는 더 적은 모델 파라미터가 필요하다. 예를 들어 마지막 레이어에 L2 패널티 가중치를 추가하여 실험했을때 논문과 유사하게 76% 까지 정확도를 높일 수 있었다.

  • 드롭아웃으로 인해 학습 데이타는 평가셋에 비해 훨씬 낮은 손실, 정확도로 시작된다.

Extensions and Exercises

이외에 모델 성능을 개선할 수 있는 유용한 케이스를 몇 가지 소개한다.

  • 사전 훈련된 word2vec 벡터를 사용하여 임베딩을 초기화한다. 그런데 앞서 모델 챕터에서 언급했듯이 서울대 논문에서는 별 효과가 없었다고 한다.2

  • 오버피팅 방지를 위해 망에 L2 정규화를 추가하고 드롭아웃 비율 증가를 실험한다. 참고로 코드에는 이미 L2 정규화 옵션이 포함되어 있으며 비활성화 되어 있다.

  • 가중치 업데이트 및 레이어 액션에 대한 히스토그램 써머리를 추가하고 텐서보드에서 시각화한다.

코드

References

  1. UNDERSTANDING CONVOLUTIONAL NEURAL NETWORKS FOR NLP

  2. 컨볼루션 신경망 기반 대용량 텍스트 데이터 분류 기술 – 2015년 동계학술발표회 논문집

  3. Convolutional Neural Networks for Sentence Classification

  4. A Sensitivity Analysis of (and Practitioners’ Guide to) Convolutional Neural Networks for Sentence Classification

출처 : 카카오 박상길님 블로그

http://docs.likejazz.com/cnn-text-classification-tf/