AI4NLP

Tensorflow 2.0 Tutorial - Text Classification 설명 본문

Tensorflow

Tensorflow 2.0 Tutorial - Text Classification 설명

nlp user 2020. 4. 12. 17:34

Intro

포스팅을 시작하기에 앞서 시퀀스와 텍스트의 관계에 대해 이야기하고 시작해보려 합니다.

시퀀스(Sequence)의 뜻은 롱맨 영어사전에 따르면 다음과 같습니다.

"the order that something happens or exists in, or the order it is supposed to happen or exist in"

해석하자면 "어떤 것이 일어나거나 존재하는 순서 혹은 그것이 일어나거나 존재하게 되어 있는 순서."입니다.

간략하게 말하자면 순서입니다. 순서를 가지고 분석해야할 일상의 문제들은 어떤 것들이 있을까요? 주식, 날씨, 등이 있을 것입니다. 또한 주식, 날씨와 같이 순차적으로 일어나는 것들은 "이전의 상태가 현재 상태에 영향을 주는 경우"라고도 이해할 수 있을 것입니다.

그렇다면 텍스트는 순서(Sequence)와 어떤 연관이 있을까요? 하나의 예시를 들어보겠습니다. "나는 너를" 다음에는 어떤 말이 오는 것이 적절할까요? "같다"라는 말이 와서 "나는 너를 같다"가 되었다고 해보겠습니다. "나는 너를 같다"라는 문장은 누가 보아도 이상한 문장입니다. 하지만 "나는 너를 사랑한다"는 누가 보아도 말이 되는 말입니다. 우리는 이 사례를 통해서 "나는 너를"이라는 말이 주어진 상태가 다음에 나올 말에 영향을 준다는 것을 알 수 있습니다. 이러한 이유로 우리는 텍스트 데이터를 시퀀스라고 생각하고, 다룰 수 있게 됩니다.

이번 챕터에서는 호메로스의 3가지 다른 영어 번역본을 훈련 데이터로 사용하여 임의의 문장에 대해 번역가가 누구인지 맞추는 텍스트 분류(Text Classification)를 다뤄보려 합니다.

(코드 원본 출처 : https://www.tensorflow.org/tutorials/load_data/text)

 

RNN Network

코드에 필요한 라이브러리들을 불러옵니다.

import tensorflow as tf
import tensorflow_datasets as tfds
import os

 

데이터 주소를 설정하고, 데이터를 다운로드한 후에 상위 디렉토리 위치를 parent_dir에 담습니다.

 

DIRECTORY_URL = '<https://storage.googleapis.com/download.tensorflow.org/data/illiad/>'
FILE_NAMES = ['cowper.txt', 'derby.txt', 'butler.txt']

for name in FILE_NAMES:
	text_dir = tf.keras.utils.get_file(name, origin=DIRECTORY_URL+name)

parent_dir = os.path.dirname(text_dir)

 

tf.data.Dataset에서 쓸 하이퍼 파라미터를 설정합니다.

 

BUFFER_SIZE = 50000
BATCH_SIZE = 64
TAKE_SIZE = 5000

 

FILE_NAMES를 enumerate를 이용하여 i와 file_name로 불러옵니다.

그 후, i는 정답(label)로 file_names는 parent_dir를 이용하여 (텍스트, 정답) 형태의 tf.data.TextLineDatase로 불러옵니다.

 

def labeler(example, index):
	return example, tf.cast(index, tf.int64)
    
labeled_data_sets = []

for i, file_name in enumerate(FILE_NAMES):
	lines_dataset = tf.data.TextLineDataset(os.path.join(parent_dir, file_name))
    labeled_dataset = lines_dataset.map(lambda ex: labeler(ex, i))
    labeled_data_sets.append(labeled_dataset)

 

리스트 형태인 labeled_data_sets를 concatenate을 이용하여 합쳐줍니다.

 

all_labeled_data = labeled_data_sets[0]
for labeled_dataset in labeled_data_sets[1:]:
	all_labeled_data = all_labeled_data.concatenate(labeled_dataset)

all_labeled_data = all_labeled_data.shuffle( BUFFER_SIZE, reshuffle_each_iteration=False)

 

take 명령어를 이용하여 5개 데이터의 예시를 확인해봅니다.

데이터 형태는 (<tf.Tensor>,<tf.Tensor>)입니다.

 

for ex in all_labeled_data.take(5):    
	print(ex)
(<tf.Tensor: id=702292, shape=(), dtype=string, numpy=b'presently Nestor rose to speak. "Son of Tydeus," said he, "in war your'>, <tf.Tensor: id=702293, shape=(), dtype=int64, numpy=2>)
(<tf.Tensor: id=702294, shape=(), dtype=string, numpy=b'Shame, Menelaus, shall to thee redound'>, <tf.Tensor: id=702295, shape=(), dtype=int64, numpy=0>)
(<tf.Tensor: id=702296, shape=(), dtype=string, numpy=b'Who oversees all Ilium, that he send'>, <tf.Tensor: id=702297, shape=(), dtype=int64, numpy=0>)
(<tf.Tensor: id=702298, shape=(), dtype=string, numpy=b'No thought have I for these or aught beside,'>, <tf.Tensor: id=702299, shape=(), dtype=int64, numpy=1>)
(<tf.Tensor: id=702300, shape=(), dtype=string, numpy=b'he is some god who is angry with the Trojans about their sacrifices,'>, <tf.Tensor: id=702301, shape=(), dtype=int64, numpy=2>)

 

tensorflow_datasets에서 제공하는 토크나이져를 이용하여 토크나이징해서 단어 사전 vocabulary_set을 만듭니다.

vocabulary_set의 length를 이용하여 만든 vocab_size는 모델 구성시 임베딩 레이어의 파라미터 크기로 쓰입니다.

 

tokenizer = tfds.features.text.Tokenizer()

vocabulary_set = set()
for text_tensor, _ in all_labeled_data:
    some_tokens = tokenizer.tokenize(text_tensor.numpy())
    vocabulary_set.update(some_tokens)

vocab_size = len(vocabulary_set)
print(vocab_size)
17178

 

tfds.features.text.TokenTextEncoder의 기능을 이용하여 텍스트 ←→ 단어 인덱스를 변환하는 코드들입니다.

 

encoder = tfds.features.text.TokenTextEncoder(vocabulary_set)
example_text = next(iter(all_labeled_data))[0].numpy()
print(example_text)
b'Lies store of raiment, rich and rare, the work'

 

encoded_example = encoder.encode(example_text)
print(encoded_example)
[14677, 8318, 15770, 889, 8059, 6832, 16016, 82, 1659]

 

위 예시의 encode 기능을 이용하여 문장을 벡터(숫자)로 변환합니다.

 

def encode(text_tensor, label):
    encoded_text = encoder.encode(text_tensor.numpy())
    return encoded_text[], label

def encode_map_fn(text, label):
    return tf.py_function(encode, inp=[text, label], Tout=(tf.int64, tf.int64))

all_encoded_data = all_labeled_data.map(encode_map_fn)

 

all_encoded_data에서 TAKE_SIZE만큼의 데이터를 이용하여 test_data만들어 주고, train_data는 BUFFER_SIZE를 이용하여 셔플해줍니다.

shuffle 기능을 이용하면 BUFFER_SIZE만큼 버퍼를 채우고, 버퍼에서 무작위로 샘플링하여 추출된 값을 새 값으로 바꿉니다.

BUFFER_SIZE는 데이터 셋의 크기보다 크면 shuffle은 원활히 작동합니다.

 

train_data = all_encoded_data.skip(TAKE_SIZE).shuffle(BUFFER_SIZE)
train_data = train_data.padded_batch(BATCH_SIZE, padded_shapes=([-1],[]))

test_data = all_encoded_data.take(TAKE_SIZE)
test_data = test_data.padded_batch(BATCH_SIZE, padded_shapes=([-1],[]))

 

만들어진 테스트 데이터에서 첫 원소를 추출해보면 이전 결과였던 [14677, 8318, 15770, 889, 8059, 6832, 16016, 82, 1659]과는 달라진 점을 볼 수 있습니다.

바로 기존 숫자들 뒤에 0이 붙었다는 점입니다.

0은 패딩이라고 해서 입력값의 크기를 네트워크에 맞게 고정시켜 주는 역할을 합니다.

이 코드에서는 padded_batch(BATCH_SIZE, padded_shapes=([-1],[]))에서 -1을 이용하여 패딩을 하였습니다.

패딩 크기는 랜덤 샘플링되는 결과에 따라 그때그때 바뀝니다.

 

sample_text, sample_labels = next(iter(test_data))

print(sample_text[0], sample_labels[0])
tf.Tensor([14677 8318 15770 889 8059 6832 16016 82 1659 0 0 0 0 0 0], shape=(15,), dtype=int64) 
tf.Tensor(1, shape=(), dtype=int64)

 

0의 값을 갖는 패딩이 추가되었으므로, vocab_size를 하나 늘려줍니다.

기존에는 패딩에 대한 vocab 정보가 없었습니다.

 

vocab_size += 1

 

tf.keras.Sequential()을 호출하여 model.add로 레이어들을 추가해줍니다.

Embedding 레이어를 보시면 vocab_size만큼을 파라미터로 쓰는 것을 확인하실 수 있습니다.

BiLSTM 모델에 3개의 Dense(Fully Connected Layer)가 추가된 형태의 모델입니다.

여기에서는 정답값이 0,1,2 3개 이므로 최종 출력값은 3으로 하고, softmax를 이용하여 분류하도록 모델을 구성하였습니다.

 

model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(vocab_size, 64))
model.add(tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(64)))

for units in [63, 64]:
    model.add(tf.keras.layers.Dense(units, activation='relu'))

model.add(tf.keras.layers.Dense(3, activation='softmax'))

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.summary()

 

model.summary()를 이용하여 모델의 개략적인 구조를 확인해봅니다.

 

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding (Embedding)        (None, None, 64)          1099456   
_________________________________________________________________
bidirectional (Bidirectional (None, 128)               66048     
_________________________________________________________________
dense (Dense)                (None, 63)                8127      
_________________________________________________________________
dense_1 (Dense)              (None, 64)                4096      
_________________________________________________________________
dense_2 (Dense)              (None, 3)                 195       
=================================================================
Total params: 1,177,922
Trainable params: 1,177,922
Non-trainable params: 0

 

model.fit()을 이용하여, 2 에폭만큼 모델을 학습시켜봅니다.

 

model.fit(train_data, epochs=2, validation_data=test_data)
Epoch 1/2
697/697 [==============================] - 25s 36ms/step - loss: 0.5270 - accuracy: 0.7420 - val_loss: 0.0000e+00 - val_accuracy: 0.0000e+00
Epoch 2/2
697/697 [==============================] - 22s 32ms/step - loss: 0.2981 - accuracy: 0.8686 - val_loss: 0.3700 - val_accuracy: 0.8360
<tensorflow.python.keras.callbacks.History at 0x7f447c0d4f28>

 

CNN Network

 

CNN Network는 위의 코드와 달리 고정된 패딩을 사용하였고, Sequential이 아닌 Model을 사용하도록 구성했습니다.

 

import tensorflow as tf
import tensorflow_datasets as tfds
import os

DIRECTORY_URL = 'https://storage.googleapis.com/download.tensorflow.org/data/illiad/'
FILE_NAMES = ['cowper.txt', 'derby.txt', 'butler.txt']

for name in FILE_NAMES:
    text_dir = tf.keras.utils.get_file(name, origin=DIRECTORY_URL+name)

parent_dir = os.path.dirname(text_dir)

def labeler(example, index):
    return example, tf.cast(index, tf.int64)  

BUFFER_SIZE = 50000
BATCH_SIZE = 64
TAKE_SIZE = 5000

labeled_data_sets = []

for i, file_name in enumerate(FILE_NAMES):
    lines_dataset = tf.data.TextLineDataset(os.path.join(parent_dir, file_name))
    labeled_dataset = lines_dataset.map(lambda ex: labeler(ex, i))
    labeled_data_sets.append(labeled_dataset)

all_labeled_data = labeled_data_sets[0]
for labeled_dataset in labeled_data_sets[1:]:
    all_labeled_data = all_labeled_data.concatenate(labeled_dataset)

all_labeled_data = all_labeled_data.shuffle(
    BUFFER_SIZE, reshuffle_each_iteration=False)

tokenizer = tfds.features.text.Tokenizer()

vocabulary_set = set()
for text_tensor, _ in all_labeled_data:
    some_tokens = tokenizer.tokenize(text_tensor.numpy())
    vocabulary_set.update(some_tokens)

vocab_size = len(vocabulary_set)

encoder = tfds.features.text.TokenTextEncoder(vocabulary_set)

 

encode 함수를 보시면 LSTM 코드와 달리 encoded_text가 encoded_text[:20]으로 바뀌어져 있습니다. 토큰을 20개까지만 잘라서 넣겠다는 뜻입니다. 그리고 padded_shapes도 -1이 아닌 20으로 고정된 크기로 변경되어있습니다.

 

def encode(text_tensor, label):
    encoded_text = encoder.encode(text_tensor.numpy())
    return encoded_text[:20], label

def encode_map_fn(text, label):
    return tf.py_function(encode, inp=[text, label], Tout=(tf.int64, tf.int64))

all_encoded_data = all_labeled_data.map(encode_map_fn)

train_data = all_encoded_data.skip(TAKE_SIZE).shuffle(BUFFER_SIZE)
train_data = train_data.padded_batch(BATCH_SIZE, padded_shapes=([20],[]))

test_data = all_encoded_data.take(TAKE_SIZE)
test_data = test_data.padded_batch(BATCH_SIZE, padded_shapes=([20],[]))
vocab_size += 1

 

Model은 input과 output을 만들고, 마지막에서 Model(inputs=inputs, outputs=outputs)과 같은 형태로 지정해준다는 점에서 Sequential과 다릅니다. 모델 구조가 조금 복잡해보이실 수도 있으니 코드 하단에 그림 첨부해놓겠습니다.

 

from tensorflow.keras.layers import Input, Embedding,Conv1D,Dropout,MaxPooling1D,Flatten,concatenate,Dense
from tensorflow.keras import Model

inputs = Input(shape=(20,))
embedding1 = Embedding(vocab_size, 100)(inputs)
conv1 = Conv1D(filters=100, kernel_size=2, activation='relu')(embedding1)
drop1 = Dropout(0.3)(conv1)
pool1 = MaxPooling1D(pool_size=2)(drop1)
flat1 = Flatten()(pool1)


embedding2 = Embedding(vocab_size, 100)(inputs)
conv2 = Conv1D(filters=100, kernel_size=3, activation='relu')(embedding2)
drop2 = Dropout(0.3)(conv2)
pool2 = MaxPooling1D(pool_size=2)(drop2)
flat2 = Flatten()(pool2)


embedding3 = Embedding(vocab_size, 100)(inputs)
conv3 = Conv1D(filters=100, kernel_size=4, activation='relu')(embedding3)
drop3 = Dropout(0.3)(conv3)
pool3 = MaxPooling1D(pool_size=2)(drop3)
flat3 = Flatten()(pool3)

merged = concatenate([flat1, flat2, flat3])

dense1 = Dense(10, activation='relu')(merged)
outputs = Dense(3, activation='softmax')(dense1)
model = Model(inputs=inputs, outputs=outputs)

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])

model.summary()

 

model.summary()를 이용하여 모델의 구조를 확인해봅니다.

 

Model: "model"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
input_1 (InputLayer)            [(None, 20)]         0                                            
__________________________________________________________________________________________________
embedding (Embedding)           (None, 20, 100)      1717900     input_1[0][0]                    
__________________________________________________________________________________________________
embedding_1 (Embedding)         (None, 20, 100)      1717900     input_1[0][0]                    
__________________________________________________________________________________________________
embedding_2 (Embedding)         (None, 20, 100)      1717900     input_1[0][0]                    
__________________________________________________________________________________________________
conv1d (Conv1D)                 (None, 19, 100)      20100       embedding[0][0]                  
__________________________________________________________________________________________________
conv1d_1 (Conv1D)               (None, 18, 100)      30100       embedding_1[0][0]                
__________________________________________________________________________________________________
conv1d_2 (Conv1D)               (None, 17, 100)      40100       embedding_2[0][0]                
__________________________________________________________________________________________________
dropout (Dropout)               (None, 19, 100)      0           conv1d[0][0]                     
__________________________________________________________________________________________________
dropout_1 (Dropout)             (None, 18, 100)      0           conv1d_1[0][0]                   
__________________________________________________________________________________________________
dropout_2 (Dropout)             (None, 17, 100)      0           conv1d_2[0][0]                   
__________________________________________________________________________________________________
max_pooling1d (MaxPooling1D)    (None, 9, 100)       0           dropout[0][0]                    
__________________________________________________________________________________________________
max_pooling1d_1 (MaxPooling1D)  (None, 9, 100)       0           dropout_1[0][0]                  
__________________________________________________________________________________________________
max_pooling1d_2 (MaxPooling1D)  (None, 8, 100)       0           dropout_2[0][0]                  
__________________________________________________________________________________________________
flatten (Flatten)               (None, 900)          0           max_pooling1d[0][0]              
__________________________________________________________________________________________________
flatten_1 (Flatten)             (None, 900)          0           max_pooling1d_1[0][0]            
__________________________________________________________________________________________________
flatten_2 (Flatten)             (None, 800)          0           max_pooling1d_2[0][0]            
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 2600)         0           flatten[0][0]                    
                                                                 flatten_1[0][0]                  
                                                                 flatten_2[0][0]                  
__________________________________________________________________________________________________
dense (Dense)                   (None, 10)           26010       concatenate[0][0]                
__________________________________________________________________________________________________
dense_1 (Dense)                 (None, 3)            33          dense[0][0]                      
==================================================================================================
Total params: 5,270,043
Trainable params: 5,270,043
Non-trainable params: 0
__________________________________________________________________________________________________

 

model.fit()을 이용하여, 2 에폭만큼 모델을 학습시켜봅니다.

 

model.fit(train_data, epochs=2, validation_data=test_data)

 

Epoch 1/2
697/697 [==============================] - 32s 46ms/step - loss: 0.4707 - accuracy: 0.7745 - val_loss: 0.0000e+00 - val_accuracy: 0.0000e+00
Epoch 2/2
697/697 [==============================] - 31s 45ms/step - loss: 0.2463 - accuracy: 0.8954 - val_loss: 0.3569 - val_accuracy: 0.8390

 

 

CNN - Model Architecture

 

Comments