TensorFlow에서 텍스트 분류를 위한 CNN 구현

이 게시물에서는 Kim Yoon의 “Convolutional Neural Networks for Sentence Classification“과 유사한 모델을 구현할 것 입니다. 이 논문에 제시된 모델은 Sentiment Analysis와 같은 다양한 텍스트 분류 작업에서 우수한 분류 성능을 달성하고 새로운 텍스트 분류 아키텍처의 표준 기준이되었습니다.

이 게시물은 이미 NLP에 적용된 Convolutional Neural Networks의 기초 지식에 익숙하다고 가정하고 있습니다. 그렇지 않다면 필요한 배경을 얻기 위해 “Understanding Convolutional Neural Networks for NLP“를 먼저 읽는 것이 좋습니다.

데이터 및 사전 처리

이 게시물에서 사용할 데이터세트는 원래 논문에 사용된 데이터세트 중 하나인 Rotten Tomatoes 의 Movie Review 데이터입니다 . 데이터세트에는 10,662개의 예제 검토 문장이 포함되어 있으며 절반은 긍정이고 절반은 부정입니다. 데이터세트에는 약 20k개의 어휘가 있습니다. 이 데이터세트는 매우 작기 때문에 강력한 모델을  많이 사용하게 될 것입니다. 또한 데이터세트에는 공식적인 학습/테스트 데이터 분할이 되어 있지 않으므로 데이터의 10%를 테스트 세트로 사용하기만 하면됩니다. 원래의 논문은 데이터에 대해 10배 교차 검증에 대한 결과를 보고 했습니다.

이 게시물의 데이터 사전 처리 코드는 검토 하지 않지만 Github에서 사용할 수 있으며 다음을 수행합니다.

  1. 원시 데이터 파일에서 긍정적이고 부정적인 문장을 로드합니다.

  2. 원 논문과 동일한 코드를 사용하여 텍스트 데이터를 정제합니다.

  3. 각 문장을 최대 문장 길이 59단어로 채웁니다.  다른 모든 문장에 특수한 <PAD>토큰을 추가하여 59 단어로 만듭니다. 문장을 같은 길이로 채우는 것은 배치의 각 예제가 동일한 길이여야 하므로 효율적으로 데이터를 배치 할 수 있기 때문에 유용합니다.

  4. 어휘 색인을 작성하고 각 단어를 0에서 18,765 사이의 정수(어휘 크기)로 매핑합니다. 각 문장은 정수의 벡터가됩니다.

모델

이 게시글에서 우리가 구축할 네트워크는 대략 다음과 같습니다 :

 

 

첫번째 레이어는 단어를 저차원 벡터에 포함합니다. 다음 레이어는 다중 필터 크기를 사용하여 embedded word vector에 대해 convolution을 수행합니다. 예를 들어 한번에 3, 4 또는 5단어 이상으로 이동합니다. 다음으로, convolution 레이어의 결과를 긴 피처 벡터로 최대 풀링하고, dropout 정규화를 추가하고, softmax 레이어를 사용하여 결과를 분류합니다.

이 글은 교육용 게시물이기 때문에 원래의 논문에서 모델을 조금 단순화하기로 하겠습니다.

  • word embedding을 위해 사전 훈련된 word2vec 벡터를 사용하지 않을 것 입니다. 대신 처음부터 embedding을 배웁니다.

  • weight vector에 대해 L2 norm constraint를 적용하지 않을 것입니다. 문장 분류를 위한 Convolutional Neural Network에 대한 민감도 분석(및 실무자 가이드)은 제약 조건이 최종 결과에 거의 영향을 미치지 않는다는 것을 발견했습니다.

  • 원본 논문은 정적 및 비정적 word vector인 두개의 입력 데이터 채널을 사용하여 실험합니다. 하나의 채널 만 사용합니다.

코드에 위의 확장을 추가하는 것은 비교적 간단합니다(몇 줄의 코드). 게시물 마지막에 있는 연습을 살펴보십시오.

시작합니다.

구현

다양한 하이퍼 파라미터 구성을 허용하기 위해 코드를 TextCNN클래스에 넣고 init함수에서 모델 그래프를 생성합니다.

import tensorflow as tf
import numpy as np

class TextCNN(object):
“””
A CNN for text classification.
Uses an embedding layer, followed by a convolutional, max-pooling and softmax layer.
“””
def __init__(
self, sequence_length, num_classes, vocab_size,
embedding_size, filter_sizes, num_filters):
# Implementation…

클래스를 인스턴스화하려면 다음 인수를 전달합니다.

  • sequence_length- 문장의 길이. 모든 문장의 길이가 같도록 패딩을 합니다 (데이터세트의 경우 59개).

  • num_classes – 출력 레이어의 클래스 수 (이 경우 두 개 (긍정 및 부정)).

  • vocab_size- 어휘의 크기. 이는 [vocabulary_size, embedding_size] 형태를 갖는 임베디드 레이어의 크기를 정의하는 데 필요 .

  • embedding_size – embedding의 차원.

  • filter_sizes- convolutional filter에서 다루기를 원하는 단어의 수.

  • num_filters – 필터 크기 당 필터 수 (위 참조).

Placeholder 입력

먼저 네트워크에 전달하는 입력 데이터를 정의합니다.

# Placeholders for input, output and dropout
self.input_x = tf.placeholder(tf.int32, [None, sequence_length], name=”input_x”)
self.input_y = tf.placeholder(tf.float32, [None, num_classes], name=”input_y”)
self.dropout_keep_prob = tf.placeholder(tf.float32, name=”dropout_keep_prob”)

tf.placeholder는 훈력이나 테스트 시간에 네트워크를 실행할 때 네트워크에 공급하는 placeholder 변수를 만듭니다. 두번째 인수는 입력 텐서의 모양입니다. None은 그 차원의 길이가 무엇이든 될 수 있음을 의미합니다. 우리의 경우 첫번째 차원은 일괄 처리 크기이며, None을 사용하면 네트워크에서 임의로 크기가 조정된 일괄 처리를 할 수 ​​있습니다.

dropout 레이어에 뉴런을 유지할 확률은 학습중에만 dropout이 가능하기 때문에 네트워크 입력입니다. 모델을 평가할 때 사용하지 않도록 설정합니다(나중에 자세히 설명).

레이어 포함

우리가 정의하는 첫번째 레이어는 어휘 단어 인덱스를 저차원 벡터 표현으로 매핑하는 임베디드 레이어입니다. 이것은 본질적으로 우리가 데이터로부터 배울 수 있는 룩업 테이블입니다.

with tf.device(‘/cpu:0’), tf.name_scope(“embedding”):
W = tf.Variable(
tf.random_uniform([vocab_size, embedding_size], -1.0, 1.0),
name=”W”)
self.embedded_chars = tf.nn.embedding_lookup(W, self.input_x)
self.embedded_chars_expanded = tf.expand_dims(self.embedded_chars, -1)

우리는 여기에 몇 가지 새로운 기능을 사용하고 있습니다.

  • tf.device(“/cpu:0”)는 CPU에서 연산이 강제 실행됩니다. 기본적으로 TensorFlow는 GPU에 사용할 수 있는 작업을 넣으려고 하지만 임베디드 구현에는 현재 GPU 지원이 없으며 GPU에 배치하면 오류가 발생합니다.

  • tf.name_scope는 이름이 “embedding”인 새 Name Scope를 만듭니다 . 이 scope는 “embedding”이라는 최상위 노드에 모든 작업을 추가하여 TensorBoard에서 네트워크를 시각화 할 때 멋진 레이어를 얻습니다.

W는 우리가 훈련 도중 배우는 embedding matrix입니다. random uniform distribution을 사용하여 초기화합니다. tf.nn.embedding_lookup은 실제 임베딩 작업을 만듭니다. 임베딩 작업의 결과는 3차원 텐서 형태 [None, sequence_length, embedding_size] 입니다.

TensorFlow의 convolutional conv2d operation에서는 배치, 폭, 높이 및 채널에 해당하는 크기의 4차원 텐서가 필요합니다. embedding한 결과에는 채널 크기가 포함되지 않으므로 수동으로 추가하여  [None, sequence_length, embedding_size, 1]의 모양을 유지합니다.

Convolution과 Max-Pooling Layer

이제 우리는 최대 풀링(max-pooling)이 뒤 따르는 convolutional 레이어를 만들 준비가 되었습니다. 크기가 다른 필터를 사용한다는 것을 기억하십시오. 각각의 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은 비선형성을 convolution 출력에 적용한 결과입니다. 각 필터는 전체 포함에 대해 슬라이드하지만 단어의 수에 따라 다릅니다. “VALID” padding은 가장자리를 채우지 않고 문장 위에 필터를 밀어 넣음으로써 좁은 convolution을 수행하여  [1, sequence_length – filter_size + 1, 1, 1] 모양의 결과를 얻을 수 있음을 의미합니다. 특정 필터 크기의 출력에 대해 max-pooling을 수행하면 텐서 형태가 [batch_size, 1, 1, num_filters] 됩니다. 이것은 본질적으로 피처 벡터이며, 마지막 차원은 피처와 일치합니다. 일단 각 필터 크기에서 모든 풀링된 출력 텐서를 갖게 되면 이를 하나의 긴 피처 벡터 형태로 결합합니다. NLP 에 대한 Convolutional Neural Network의 이해를 참조하여 직감을 얻을 수도 있습니다. TensorBoard에서 작업을 시각화하면 도움이됩니다.

Dropout 레이어

Dropout은 convolutional neural network를 규칙화하는 가장 보편적인 방법입니다. dropout의 배경은 간단합니다. dropout 레이어는 뉴런의 일부를 확률적으로 “비활성화”합니다. 이것은 뉴런이 동시에 적응하는 것을 방지하고 개별적으로 유용한 기능을 배우도록 강요합니다. 활성화된 뉴런의 비율은 네트워크에 dropout_keep_prob에 대한 입력에 의해 정의됩니다. 이것을 훈련 중 0.5와 같은 것으로 설정하고 평가 중에는 1 (dropout 비활성화)으로 설정합니다.

# Add dropout

with tf.name_scope(“dropout”):

    self.h_drop = tf.nn.dropout(self.h_pool_flat, self.dropout_keep_prob)

점수 및 예측

max-pooling의 피처 벡터를 사용하면 (dropout이 적용된 상태에서) 행렬 곱셈을 수행하고 가장 높은 점수를 가진 클래스를 선택하여 예측을 생성할 수 있습니다. 원시 점수를 정규화된 확률로 변환하는 softmax 함수를 적용 할 수도 있지만 최종 예측은 변경되지 않습니다.

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와 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)

여기에서 tf.nn.softmax_cross_entropy_with_logits는 점수와 올바른 입력 레이블이 주어지면 각 클래스의 교차 엔트로피 손실을 계산하는 편리한 함수입니다. 그런 다음 손실의 평균을 취합니다. 합계를 사용할 수도 있지만 다른 배치 크기와 train / dev 데이터에서 손실을 비교하는 것이 더 어렵습니다.

정확도에 대한 표현도 정의합니다. 이는 훈련 및 테스트 중에 추적할 수있는 유용한 양입니다.

# Calculate Accuracy

with tf.name_scope(“accuracy”):

    correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1))

    self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, “float”), name=”accuracy”)

네트워크 시각화

이제 네트워크 정의가 완료되었습니다. 전체 코드 네트워크 정의 코드는 여기에서 볼 수 있습니다 . 큰 그림을 얻으려면 TensorBoard 에서 네트워크를 시각화 할 수도 있습니다 .

 

 

학습 절차

네트워크에 대한 학습 과정을 정의하기 전에 우리는 TensorFlow를 사용하는 방법에 대해 Session과 Graph 기본을 이해할 필요가 있습니다 . 이미 이 개념을 잘 알고 있다면 이 섹션을 건너 뛰어도 좋습니다.

TensorFlow에서는 그래프 작업을 실행하는 환경과 변수, 대기열에 대한 상태를 포함합니다. 각 세션은 하나의 그래프에서 작동합니다. 변수 및 작업을 만들 때 세션을 명시적으로 사용하지 않으면 TensorFlow에서 생성한 현재 기본 세션을 사용하고있는 것입니다. session.as_default() 블록 내에서 명령을 실행하여 기본 세션을 변경할 수 있습니다.

Graph는 연산과 텐서를 포함합니다. 프로그램에서 여러 그래프를 사용할 수 있지만 대부분의 프로그램은 하나의 그래프만 필요합니다. 동일한 그래프를 여러 세션에서 사용할 수 있지만 한 세션에서 여러 그래프를 사용할 수는 없습니다. TensorFlow는 항상 기본 그래프를 생성하지만 아래에서처럼 그래프를 수동으로 만들고 새 기본값으로 설정할 수도 있습니다. 세션과 그래프를 명시적으로 작성하면 더 이상 필요하지 않을 때 자원이 올바르게 릴리스됩니다.

with tf.Graph().as_default():

    session_conf = tf.ConfigProto(

      allow_soft_placement=FLAGS.allow_soft_placement,

      log_device_placement=FLAGS.log_device_placement)

    sess = tf.Session(config=session_conf)

    with sess.as_default():

        # Code that operates on the default graph and session comes here…

allow_soft_placement의 설정은 TensorFlow가 바람직한 장치가 존재하지 않을 때 구현된 특정 작업과 장치에서 양보할 수 있습니다. 예를 들어, 코드가 GPU에 작업을 배치하고 GPU가 없는 컴퓨터에서 코드를 실행하는 경우 allow_soft_placement를 사용하지 않으면 오류가 발생합니다.  이 경우 log_device_placement가 설정되어 TensorFlow는 작업을 배치하는 장치 (CPU 또는 GPU)에 로그인합니다. 이것은 디버깅에 유용합니다. FLAGS는 커맨드라인 인수입니다.

CNN 인스턴스화 및 손실 최소화

우리의 TextCNN모델을 인스턴스화 할 때 정의된 모든 변수와 연산은 위에서 작성한 기본 그래프와 세션에 배치됩니다.

cnn = TextCNN(

    sequence_length=x_train.shape[1],

    num_classes=2,

    vocab_size=len(vocabulary),

    embedding_size=FLAGS.embedding_dim,

    filter_sizes=map(int, FLAGS.filter_sizes.split(“,”)),

    num_filters=FLAGS.num_filters)

다음으로, 우리는 네트워크의 손실 기능을 최적화하는 방법을 정의합니다. TensorFlow에는 몇가지 내장 옵티 마이저가 있습니다. Adam 최적화 도구를 사용합니다.

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)

여기 train_op에 우리가 파라미터에 gradient 업데이트를 수행하기 위해 실행할 수있는 새로 생성된 작업이 있습니다. train_op의 각 실행은 학습 단계입니다. TensorFlow는 어떤 변수가 “학습 가능”한지 자동으로 파악하고 gradient를 계산합니다. global_step변수를 정의하고 이를 옵티마이저에 전달함으로써 TensorFlow는 학습 단계 계산을 처리 할 수 ​​있습니다. 글로벌 단계는 사용자가 train_op 실행 때 마다 자동으로 1씩 증가합니다.

Summaries

TensorFlow에는 학습 및 평가 과정에서 다양한 양을 추적하고 시각화할 수 있는 요약 개념이 있습니다. 예를 들어 시간 경과에 따른 손실 및 정확성의 변화를 추적하고 싶을 수 있습니다. 레이어 활성화의 막대 그래프와 같이 더 복잡한 양을 추적 할 수도 있습니다. 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)

여기서는 학습 및 평가에 대한 요약을 개별적으로 추적합니다. 우리의 경우 이 수치는 같은 양이지만, 학습 중에 추적하고 싶은 양 (파라미터 업데이트 값과 같음)이 있을 수 있습니다. tf.merge_summary 는 여러 요약 작업을 실행할 수 있는 단일 작업으로 병합하는 편리한 기능입니다.

 

 

원문

http://www.wildml.com/2015/12/implementing-a-cnn-for-text-classification-in-tensorflow/