케라스 창시자에게 배우는 딥러닝_11장(1)

코넬·2023년 2월 16일
0

DeepLearning_Keras

목록 보기
11/13
post-thumbnail

🗒️ 텍스트를 위한 딥러닝

자연어 처리 소개

자연어란?

한국어나 영어 같은 사람의 언어를 어셈블리어, LISP, XML 과 같은 기계를 위해 고안된 언어와 구별하기 위해 자연어(natural language)라고 부른다.
자연어는 복잡하고, 모호하고, 불규칙하고, 끊임없이 변화한다. 자연어를 이해할 수 있는 알고리즘을 만드는 것은 큰 일이며, 텍스트는 사람의 의사소통을 위해 만들어졌기 때문에 이를 이해하는 능력을 기계가 달성하기에는 어려움이 있었다.

초기에는 LISP의 규칙 집합을 작성하는 것처럼 영어의 규칙 집합을 작성할 수 있을 것이라고 생각하여 응용 언어학(applied linguistics) 의 입장에서 시도되었으나, 언어는 규칙에 맞게 쉽게 체계화할 수 없었다. 이 때 나타난 것이 챗봇과 기계번역 수행 등이였다.
시간이 지나 1980년대 후반부터 빠른 컴퓨터와 많은 데이터를 사용할 수 있게 되면서 더 나은 대안이 등장하였다. 자연어 처리에 머신 러닝 방법을 적용하기 시작하였고,

  • 초기 시도는 결정 트리 기반,
  • 로지스틱 회귀와 같은 통계적 방법이 이어졌고,
  • 시간이 지남에 따라 학습된 파라미터를 가진 모델 이 완전히 자리를 잡았다.

이것이 현대적인 NLP 이며, 입력으로 언어를 받아 유용한 어떤 것을 반환하는 방식이다.
NLP의 발전에 따라 NPL 도구(결정 트리, 로지스틱 회귀) 는 순환 신경망, 특히 LSTM의 언어 이해 능력을 분석하기 시작하였다.

  • 2015년 초 순환 신경망에 대한 관심이 커지면서 케라스가 가장 처음 오픈 소스로 사용하기 쉬운 LSTM 구현을 제공하였다.
  • 2015년부터 2017년까지 순환 신경망이 급성장하는 NLP 분야를 지배하며, 양방향 LSTM 모델이 요약에서 질문-대답, 기계 번역까지 많은 중요한 작업에서 최고 수준의 성능을 달성하였다.
  • 마지막으로 2017-2018년 즈음에 새로운 아키텍처가 나와 RNN을 대체하였다.이것이 바로 트랜스포머(transformer) 이며, 오늘날 대부분의 NLP시스템은 트랜스포머를 기반으로 한다.

💪🏻 텍스트 데이터 준비

자, 이제 본격적으로 NLP를 진행해볼까?

딥러닝 모델은 수치 텐서 만 처리할 수 있기 때문에, 원시 텍스트를 입력으로 사용할 수 없다. 따라서 텍스트 벡터화(vectorization) 를 통해 텍스트를 수치 텐서로 바꿔줘야한다.

☝🏻 첫번째, 텍스트 벡터화 진행하기 !

  • 먼저 처리하기 쉽도록 텍스트를 표준화(standardization) 한다. (소문자로 바꾸기, 구두점 제거하기 등)
  • 다음으로 텍스트를 토큰(token) 이라는 단위로 분할한다. (예를 들어, 문자, 단어, 단어의 그룹 등) 이를 토큰화(tokenization) 라고 부른다.
  • 마지막으로 각 토큰을 수치 벡터로 바꾼다. 먼저 데이터에 등장하는 모든 토큰을 인덱싱(indexing)한다.

먼저 처리하기 쉽도록 텍스트를 표준화(standardization) 한다.

가장 간단하고 널리 사용되는 표준화 방법은 "소문자로 바꾸고 구두점 문자를 삭제" 하는 것이다.
이런 표준화 기법을 사용하면 모델에 필요한 훈련 데이터가 줄어들고 일반화가 더 잘된다. 'Sunset' 과 'sunset'이 같은 의미라는 것을 학습하기 위해 많은 샘플이 필요하지 않고, 훈련세트에 하나만 있더라도 다른 같은 의미를 이해할 수 있다.

다음으로 텍스트를 토큰(token) 이라는 단위로 분할한다.

세가지 방법으로 토큰화를 수행할 수 있다.

  • 단어 수준 토큰화 : 토큰이 공백으로 (또는 구두점으로) 구분된 문자열이다. 비슷한 다른 방법은 가능한 경우 단어를 부분 단어(subword)로 더 나누는 경우이다. (예를 들어, staring을 star+ing으로 나누는 경우)
  • N-그램 토큰화 : 토큰이 N개의 연속된 단어 그룹이다. (예를 들어 he was는 2-그램 토큰)
  • 문자 수준 토큰화 : 각 문자가 하나의 토큰이다. 실제로는 잘 사용안되나 음성 인식 등에서만 사용한다.

여기서, 두 종류의 텍스트 처리 모델이 있는데, 단어의 순서를 고려하는 시퀀스 모델(sequence model) 과 입력 단어의 순서를 무시하고 집합으로 다루는 BoW모델(Bag-of-words model) 이다. 시퀀스 모델을 만든다면 단어 수준 토큰화를 사용하고, BoW 모델을 만든다면 N-그램 토큰화를 사용한다. (N-그램 토큰화는 모델에 국부적인 단어 순서에 대한 소량의 정보를 주입하는 방식이다.)

마지막으로 각 토큰을 수치 벡터로 바꾼다. 먼저 데이터에 등장하는 모든 토큰을 인덱싱(indexing)한다.

텍스트를 토큰으로 나눈 후 각 토큰을 수치 표현으로 인코딩한다. 훈련 데이터에 있는 모든 토큰의 인덱스(어휘 사전)를 만들어 어휘 사전의 각 항목에 고유한 정수를 할당하는 방법을 사용한다.

텍스트 벡터화, 코드로 구현해보자 !

실전에서 빠르고 효율적인 케라스 Text Vectorization 층을 사용하자.

from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
    output_mode="int",
)

정수 인덱스로 인코딩된 단어 시퀀스를 반환하도록 층을 설정한다. 여러가지 다른 출력 모드도 존재한다.
기본적으로 Textvectorization층은 텍스트 표준화를 위해 소문자로 바꾸고 구두점을 제거하며 토큰화를 위해 공백으로 나눈다. 어떤걸 기준을 두느냐에 따라 사용자 정의 함수도 만들 수 있는데, 이 때는 일반적인 파이썬 문자열이 아니라 tf.string 텐서 를 처리해야한다.

import re
import string
import tensorflow as tf

def custom_standardization_fn(string_tensor):
    lowercase_string = tf.strings.lower(string_tensor) #문자열을 소문자로 바꾸기
    return tf.strings.regex_replace( #구두점을 빈 문자열로 바꾸기
        lowercase_string, f"[{re.escape(string.punctuation)}]", "")

def custom_split_fn(string_tensor):
    return tf.strings.split(string_tensor) #공백을 기준으로 문자열을 나눈다.

text_vectorization = TextVectorization(
    output_mode="int",
    standardize=custom_standardization_fn,
    split=custom_split_fn,
)

훈련 데이터의 어휘 사전을 인덱싱하려면 문자열을 반환하는 Dataset 객체로 이 층의 adapt() 메서드 를 호출하면 된다.

dataset = [
    "I write, erase, rewrite",
    "Erase again, and then",
    "A poppy blooms.",
]
text_vectorization.adapt(dataset)

get_vocabulary() 메서트 를 사용하여 계산된 어휘 사전을 추출할 수 있다. 이는 정수 시퀀스로 인코딩된 텍스트를 단어로 다시 변환할 때 유용하다.

text_vectorization.get_vocabulary()
------------------------------------
['',
 '[UNK]',
 'erase',
 'write',
 'then',
 'rewrite',
 'poppy',
 'i',
 'blooms',
 'and',
 'again',
 'a']

자, 이렇게 텍스트 전처리에 대한 모든 내용을 배웠다. 이제는 모델링으로 넘어가보자.

🦾 단어 그룹을 표현하는 두가지 방법 : 집합과 시퀀스

단어는 범주형 특성(미리 정의된 집합에 있는 값)이고, 이를 처리하는 방법은 정해져있다. 이는 단어를 특성 공간의 차원으로 인코딩하거나 범주 벡터로 인코딩하는 방식인데, 이보다 더 중요한 것은 단어를 문장으로 구성하는 방식인 단어 순서를 인코딩하는 방법이다.
단어의 순서를 표현하는지는 여러 종류의 NLP 아키텍처를 발생시키는 핵심 질문이다.

가장 쉬운 방법은 순서를 무시하고 텍스트를 단어의 집합으로 처리하는 것인데, 이것이 BoW모델 이다.

다른 방법은 하이브리드 방식으로, 트랜스포머 아키텍처는 순서에 구애받지는 않지만 처리하는 표현에 단어 위치 정보를 주입한다. 이를 통해 순서를 고려하면서 문장의 여러 부분을 동시에 볼 수 있다. 단어 순서를 고려하기 때문에 RNN과 트랜스포머 모두 시퀀스 모델 이라고 부른다.

IMDB 영화 리뷰 데이터 준비하기

!curl -O https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xf aclImdb_v1.tar.gz #데이터 불러오기

!rm -r aclImdb/train/unsup #필요없는 디렉터리 지우기
!cat aclImdb/train/pos/4077_10.txt #데이터 모습 확인하기

다음으로 훈련 텍스트 파일에서 20%를 새로운 디렉터리로 덜어내어 검증 세트를 만들어보자.

import os, pathlib, shutil, random

base_dir = pathlib.Path("aclImdb")
val_dir = base_dir / "val"
train_dir = base_dir / "train"
for category in ("neg", "pos"):
    os.makedirs(val_dir / category)
    files = os.listdir(train_dir / category)
    random.Random(1337).shuffle(files) 
    # 코드를 여러번 실행해도 동일한 검증 세트가 만들어지도록 랜덤 시드를 지정하여 훈련 파일 목록을 섞는다.
    num_val_samples = int(0.2 * len(files))
    # 훈련 파일 중 20%를 검증 세트로 덜어낸다.
    val_files = files[-num_val_samples:]
    for fname in val_files:
        shutil.move(train_dir / category / fname,
                    val_dir / category / fname) #파일 옮기기

텍스트 파일에서 text_dataset_from_directory 유틸리티 를 적용하여 디렉터리 구조를 바탕으로 텍스트와 레이블의 배치 데이터셋을 생성한다. (훈련, 검증, 테스트를 위한 3개의 데이터셋 객체를 만든다.)

from tensorflow import keras
batch_size = 32

train_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/train", batch_size=batch_size
)
val_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/val", batch_size=batch_size
)
test_ds = keras.utils.text_dataset_from_directory(
    "aclImdb/test", batch_size=batch_size
)

단어를 집합으로 처리하기 : BoW 방식

가장 간단한 방법인 순서를 무시하고 토큰의 집합으로 인코딩을 진행한다.

  • 이진 인코딩을 사용한 유니그램 : 전체의 텍스트를 하나의 벡터로 표현할 수 있다. 벡터의 각 원소는 한 단어의 존재 유무를 표시한다.
    예를 들어 멀티-핫 이진 인코딩을 사용하면 하나의 텍스트를 어휘 사전에 있는 단어 개수만큼의 차원을 가진 벡터로 인코딩한다. 텍스트에 있는 단어에 해당하는 차원은 1이고 나머지는 0이다.)

직접 진행해보자. 먼저 원시 텍스트를 TextVectorization 층으로 처리하여 멀티-핫 인코딩된 이진 단어 벡터로 만든다. 이 층은 하나의 단어씩 처리한다.

text_vectorization = TextVectorization(
    max_tokens=20000,
    #가장 많이 등장하는 2만개 단어로 어휘 사전을 제한한다. 
    #그렇지 않으면 훈련 데이터에있는 모든 단어를 인덱싱하기 때문에, 
    #수만개의 단어가 한번 또는 두번만 등장하면 유용하지 않을 것이다. 
    #(2만개가 일반적으로 적절한 어휘 사전 크기이다)
    
    output_mode="multi_hot",
)
text_only_train_ds = train_ds.map(lambda x, y: x)
text_vectorization.adapt(text_only_train_ds)

binary_1gram_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_1gram_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_1gram_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

모델 생성 함수를 만들어보자.

from tensorflow import keras
from tensorflow.keras import layers

def get_model(max_tokens=20000, hidden_dim=16):
    inputs = keras.Input(shape=(max_tokens,))
    x = layers.Dense(hidden_dim, activation="relu")(inputs)
    x = layers.Dropout(0.5)(x)
    outputs = layers.Dense(1, activation="sigmoid")(x)
    model = keras.Model(inputs, outputs)
    model.compile(optimizer="rmsprop",
                  loss="binary_crossentropy",
                  metrics=["accuracy"])
    return model

다음으로 모델을 훈련하고 테스트 하는 코드까지 작성해보자.

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("binary_1gram.keras",
                                    save_best_only=True)
]
model.fit(binary_1gram_train_ds.cache(),
          validation_data=binary_1gram_val_ds.cache(),
          # 데이터셋의 cache()메서드를 호출하여 메모리에 캐싱한다.
          epochs=10,
          callbacks=callbacks)
model = keras.models.load_model("binary_1gram.keras")
print(f"테스트 정확도: {model.evaluate(binary_1gram_test_ds)[1]:.3f}")

여기서 테스트 정확도 89.2% 를 얻을 수 있다. 이 경우 데이터셋이 균형 잡힌 이진 분류 데이터셋이기 때문에 실제 모델을 훈련하지 않고 얻을 수 있는 단순한 기준점은 50% 이다. 외부 데이터를 활용하지 않고 이 데이터셋에서 달성할 수 있는 최상의 테스트 정확도는 약 95% 이다.

  • 이진 인코딩을 사용한 바이그램 : 하나의 개념이 여러 단어로 표현될 수 있는 경우가 있다. 이때는 순서를 무시하면 매우 파괴적이다( 예를 들어 United State 에서 두 단어는 각각 의미가 깊다.) TextVectorization 층 은 바이그램, 트라이그램을 포함하여 임의의 N-그램 을 반환할 수 있다. ngrams = N 매개변수를 전달하면 되는 것 !

똑같이 실습을 진행해보자 !

바이그램을 반환하는 TextVectorization 층을 만들어보자.

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="multi_hot",
)

이진 인코딩된 바이그램에서 훈련한 모델의 성능을 확인해보자 !

text_vectorization.adapt(text_only_train_ds)
binary_2gram_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_2gram_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
binary_2gram_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("binary_2gram.keras",
                                    save_best_only=True)
]
model.fit(binary_2gram_train_ds.cache(),
          validation_data=binary_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)
model = keras.models.load_model("binary_2gram.keras")
print(f"테스트 정확도: {model.evaluate(binary_2gram_test_ds)[1]:.3f}")
----------------------------------------------------------------------
Model: "model_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_2 (InputLayer)        [(None, 20000)]           0         
                                                                 
 dense_2 (Dense)             (None, 16)                320016    
                                                                 
 dropout_1 (Dropout)         (None, 16)                0         
                                                                 
 dense_3 (Dense)             (None, 1)                 17        
                                                                 
=================================================================
Total params: 320,033
Trainable params: 320,033
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
625/625 [==============================] - 9s 14ms/step - loss: 0.3754 - accuracy: 0.8461 - val_loss: 0.2875 - val_accuracy: 0.8892
Epoch 2/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2377 - accuracy: 0.9169 - val_loss: 0.2920 - val_accuracy: 0.8938
Epoch 3/10
625/625 [==============================] - 3s 6ms/step - loss: 0.2026 - accuracy: 0.9355 - val_loss: 0.3216 - val_accuracy: 0.8936
Epoch 4/10
625/625 [==============================] - 3s 5ms/step - loss: 0.1834 - accuracy: 0.9435 - val_loss: 0.3291 - val_accuracy: 0.8970
Epoch 5/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1778 - accuracy: 0.9457 - val_loss: 0.3400 - val_accuracy: 0.8932
Epoch 6/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1758 - accuracy: 0.9491 - val_loss: 0.3575 - val_accuracy: 0.8942
Epoch 7/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1682 - accuracy: 0.9530 - val_loss: 0.3699 - val_accuracy: 0.8954
Epoch 8/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1764 - accuracy: 0.9524 - val_loss: 0.3807 - val_accuracy: 0.8954
Epoch 9/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1706 - accuracy: 0.9534 - val_loss: 0.3842 - val_accuracy: 0.8932
Epoch 10/10
625/625 [==============================] - 3s 4ms/step - loss: 0.1726 - accuracy: 0.9541 - val_loss: 0.3879 - val_accuracy: 0.8930
782/782 [==============================] - 8s 10ms/step - loss: 0.2663 - accuracy: 0.8983
테스트 정확도: 0.898

테스트 정확도 또한 90.4%가 나온다. 크게 향상된걸 보아 국부적인 순서매우 중요하다는 것을 확인할 수 있다.

  • TF-IDF 인코딩을 사용한 바이그램 : 이 표현에서는 개별 단어나 N-그램의 등장 횟수를 카운트한 정보를 추가할 수 있다. 즉, 텍스트에 대한 단어의 히스토그램을 사용하는 것 !
    예를 들어보자. ({"the" : 2, "the cat" : 1, "cat" : 1, "cat sat" : 1, "sat" :1 ...})
    이렇게 텍스트 분류 작업을 한다면 한 샘플에 단어가 얼마나 많이 등장하는지 가 중요하다. 부정적인 단어가 많이 나오면 그 흐름은 부정적일 확률이 높은 것처럼 !

토큰 카운트를 반환하는 TextVectorization층을 만들어보자.

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="count"
)

여기서 잠깐 !

일부 단어는 텍스트에 상관없이 다른 단어보다 많이 등장한다. 여기서 "the" , "a" , "is", "are" 같은 단어들은 분류 작업에서 거의 쓸모없는 특성임에도 불구하고 항상 단어 카운트 히스토그램을 압도하기 때문에 좋지않은 영향을 끼친다. 어떻게 하면 이런 단어들의 영향을 줄일 수 있을까?

정답은 바로 정규화를 사용한다 !

전체 훈련 데이터셋에서 계산된 평균을 빼고 분산으로 나누어 단어 카운트를 정규화할 수 있다. 하지만 벡터화된 문장은 대부분 거의 전체가 0으로 구성되기 때문에, 단어의 역-문서-빈도, 즉 데이터셋에있는 모든 문서에 걸쳐 단어가 등장하는 빈도를 역으로 활용하는 (현재 문서에 단어가 등장하는 횟수인 단어 빈도로 해당 단어에 가중치를 부여하고, 데이터셋 전체에 단어가 등장하는 횟수인 문서빈도로 나누는) TF-IDF 정규화(Term Frequency-Inverse Document Frequency) 를 사용한다.

이 방식은 널리 사용되기 때문에 TextVectorization층에 구현되어있다. output_mode 매개변수를 "tf-idf"로 바꾸기만 하면 사용할 수 있다.

text_vectorization = TextVectorization(
    ngrams=2,
    max_tokens=20000,
    output_mode="tf_idf",
)

이 방식으로 새 모델을 훈련해보자.

with tf.device("cpu"):
    text_vectorization.adapt(text_only_train_ds)

tfidf_2gram_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
tfidf_2gram_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
tfidf_2gram_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

model = get_model()
model.summary()
callbacks = [
    keras.callbacks.ModelCheckpoint("tfidf_2gram.keras",
                                    save_best_only=True)
]
model.fit(tfidf_2gram_train_ds.cache(),
          validation_data=tfidf_2gram_val_ds.cache(),
          epochs=10,
          callbacks=callbacks)
model = keras.models.load_model("tfidf_2gram.keras")
print(f"테스트 정확도: {model.evaluate(tfidf_2gram_test_ds)[1]:.3f}")
-------------------------------------------------
Model: "model_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_3 (InputLayer)        [(None, 20000)]           0         
                                                                 
 dense_4 (Dense)             (None, 16)                320016    
                                                                 
 dropout_2 (Dropout)         (None, 16)                0         
                                                                 
 dense_5 (Dense)             (None, 1)                 17        
                                                                 
=================================================================
Total params: 320,033
Trainable params: 320,033
Non-trainable params: 0
_________________________________________________________________
Epoch 1/10
625/625 [==============================] - 9s 14ms/step - loss: 0.4958 - accuracy: 0.7667 - val_loss: 0.3164 - val_accuracy: 0.8742
Epoch 2/10
625/625 [==============================] - 3s 4ms/step - loss: 0.3338 - accuracy: 0.8566 - val_loss: 0.3232 - val_accuracy: 0.8660
Epoch 3/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2991 - accuracy: 0.8706 - val_loss: 0.3361 - val_accuracy: 0.8728
Epoch 4/10
625/625 [==============================] - 4s 6ms/step - loss: 0.2857 - accuracy: 0.8776 - val_loss: 0.3586 - val_accuracy: 0.8476
Epoch 5/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2806 - accuracy: 0.8821 - val_loss: 0.3618 - val_accuracy: 0.8708
Epoch 6/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2661 - accuracy: 0.8942 - val_loss: 0.3564 - val_accuracy: 0.8588
Epoch 7/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2583 - accuracy: 0.8988 - val_loss: 0.3568 - val_accuracy: 0.8632
Epoch 8/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2465 - accuracy: 0.9024 - val_loss: 0.3695 - val_accuracy: 0.8660
Epoch 9/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2436 - accuracy: 0.9031 - val_loss: 0.3923 - val_accuracy: 0.8620
Epoch 10/10
625/625 [==============================] - 3s 4ms/step - loss: 0.2445 - accuracy: 0.9037 - val_loss: 0.3865 - val_accuracy: 0.8540
782/782 [==============================] - 8s 10ms/step - loss: 0.3011 - accuracy: 0.8820
테스트 정확도: 0.882

현재 지금 예제에서는 이 방식이 도움이 안되는 것 같지만,
많은 텍스트 분류 데이터셋에서 기본 이진 인코딩에 비해 TF-IDF를 사용했을 때 일반적으로 1% 포인트의 성능을 높일 수 있다.

단어를 시퀀스로 처리하기 : 시퀀스 모델 방식

순서기반의 특성을 수동으로 만드는 대신 원시 단어 시퀀스 모델에 전달하여 스스로 이런 특성을 학습하도록 하는 것을 시퀀스 모델(sequence model)입니다.
시퀀스 모델을 구현하려면 먼저 입력 샘플을 정수 인덱스의 시퀀스로 표현해야한다.
그 다음, 각 정수를 벡터로 매핑하여 벡터 시퀀스를 얻는다. 마지막으로 이 벡터 시퀀스를 1D 컨브넷, RNN, 트랜스포머와 같이 인접한 벡터의 특징을 비교할 수 있는 층에 전달한다.

실전부터 도전!
정수 시퀀스 모델을 만들어보자. 정수 시퀀스 데이터셋을 준비한다.


from tensorflow.keras import layers

max_length = 600
max_tokens = 20000
text_vectorization = layers.TextVectorization(
    max_tokens=max_tokens,
    output_mode="int",
    output_sequence_length=max_length,
)
text_vectorization.adapt(text_only_train_ds)

int_train_ds = train_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
int_val_ds = val_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)
int_test_ds = test_ds.map(
    lambda x, y: (text_vectorization(x), y),
    num_parallel_calls=4)

다음으로 모델을 만든다. 정수 시퀀스를 벡터 시퀀스로 바꾸는 가장 간단한 방법은 정수를 원-핫 인코딩하는 것이다.
원-핫 벡터 위에 간단한 양방향 LSTM층을 추가한다.


import tensorflow as tf
inputs = keras.Input(shape=(None,),dtype="int64")
#입력은 정수입력 시퀀스
embedded = tf.one_hot(inputs, depth=max_tokens) #이진 벡터로 인코딩하기
x = layers.Bidirectional(layers.LSTM(32)) (embedded) #양방향 LSTM층을 추가한다.
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.summary()
---------------------------------------------------
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, None)]            0         
                                                                 
 tf.one_hot (TFOpLambda)     (None, None, 20000)       0         
                                                                 
 bidirectional (Bidirectiona  (None, 64)               5128448   
 l)                                                              
                                                                 
 dropout (Dropout)           (None, 64)                0         
                                                                 
 dense (Dense)               (None, 1)                 65        
                                                                 
=================================================================
Total params: 5,128,513
Trainable params: 5,128,513
Non-trainable params: 0
_________________________________________________________________

마지막으로 모델을 훈련해보자.

callbacks = [
    keras.callbacks.ModelCheckpoint("one_hot_bidir_lstm.keras",
                                    save_best_only=True)
]
model.fit(int_train_ds, validation_data=int_val_ds, epochs=10, callbacks=callbacks)
model = keras.models.load_model("one_hot_bidir_lstm.keras")
print(f"테스트 정확도: {model.evaluate(int_test_ds)[1]:.3f}")

이 모델의 훈련 결과로 정확도가 0.877정도가 나온다.

자 이러한 원시적인 방법을 사용할 시, 모델 성능이 좋지 않다.

  • 첫 번째, 입력 크기가 크기 때문이다. 각 입력 샘플은 (600,2000) 크기의 행렬로 인코딩되어있다. 따라서 양방향 LSTM이 해야할 일이 많다.
  • 두 번째, 이 모델의 정확도가 87%이므로, 이진 유니그램 모델만큼 성능이 좋지 않다.

(가장 손쉽게 원-핫 인코딩으로 단어를 벡터로 바꾸는 것은 좋은 생각이 아니다) 이 때 쓸 수 있는 방법이 단어 임베딩 이다.

성능 향상시키기 : 단어 임베딩 이해하기 !

원-핫 인코딩 벡터는 서로 모두 직교한다. 하지만 단어는 구조적인 공간을 형성한다. 즉, 단어에 공유되는 정보가 있기 때문에 동일한 의미로 사용되는 단어는 각 벡터들과 직교해서는 안된다. (예를 들어, moive와 film은 의미가 동일하기 때문에 직교하는 것이 아닌 동일하거나 매우 가까워야한다.)

두 단어 사이의 기하학적 관계는 단어 사이의 의미 관계를 반영해야한다. 즉, 두 단어 벡터 사이의 기하학적 거리가 단어 사이의 의미 거리에 연관되어있다고 생각할 수 있다.

단어 임베딩 은 정확히 사람의 언어를 구조적인 기하학적 공간에 매핑하는 것이다. 원-핫 인코딩은 희소하고 고차원적인 이진 벡터를 만들지만, 단어 임베딩은 저차원의 부동 소수점 벡터이다.

밀집 표현이라는 점 외에도 단어 임베딩은 구조적인 표현이며, 이 구조는 데이터로부터 학습된다. 비슷한 단어는 가까운 위치에 임베딩되며, 임베딩 공간의 특정 방향이 의미를 가질 수 있다.

단어 임베딩을 만드는 방법을 두가지이다.

  • 현재 작업(문서 분류, 감성 예측 등)과 함께 단어 임베딩을 학습한다. 이런 설정에서는 랜덤한 단어 벡터로 시작하여 신경망의 가중치를 학습하는 것과 같은 방식으로 단어 벡터를 학습한다.
  • 현재 풀어야 할 문제와 다른 머신 러닝 작업에서 미리 계산된 단어 임베딩을 모델에 로드한다. 이를 사전 훈련된 단어 임베딩 이라고 부른다.

Embedding 층으로 단어 임베딩 학습하기

사람의 언어를 완벽하게 매핑해서 어떤 자연어 처리 작업에도 사용할 수 있는 이상적인 단어 임베딩 공간은 아직까지 없다. 또한, 세상에는 많은 다른 언어가 있고 특정 문화와 환경을 반영하기 때문에 서로 동일하지 않다. 따라서 특정한 의미 관계의 중요성이 작업에 따라 다르기 때문에, 새로운 작업에는 새로운 임베딩 학습하는 것이 타당하다.

케라스에서는 Embedding층의 가중치를 학습하는 모듈이 존재한다 !

Embedding층 만들기 코드는 다음과 같다.

embedding_layer = layers.Embedding(input_dim=max_tokens, output_dim=256)
#임베딩 층은 적어도 2개의 매개변수가 필요하다. 가능한 토큰의 개수와 임베딩 차원이다.

Embedding층은 정수 인덱스를 밀집 벡터로 매핑하는 딕셔너리로 이해하는 것이 가장 좋다. 정수를 입력으로 받아 내부 딕셔너리에 이 정수와 관련된 벡터를 찾아 반환한다. (이 과정을 딕셔너리 룩업(look up)이라고 한다.)

단어 인덱스 - Embedding층 - 해당 단어 벡터

Embedding층의 특징은,

  • 크기가 (batch_size, sequence_length)인 랭크-2 정수 텐서를 입력으로 받는다.
  • 층을 만들 때 가중치(토큰 벡터를 위한 내부 딕셔너리)는 다른 층과 마찬가지로 랜덤하게 초기화된다. 훈련하면서 이 단어 벡터는 역전파를 통해 조정되고 후속 모델이 사용할 수 있도록 임베딩 공간을 구성한다. 훈련이 끝나면 임베딩 공간은 특정 문제에 전문화된 여러가지 구조를 가지게 된다.

모델을 만들고 성능을 확인해보자.

inputs = keras.Input(shape=(None,), dtype="int64")
embedded = layers.Embedding(input_dim=max_tokens, output_dim=256)(inputs)
x = layers.Bidirectional(layers.LSTM(32))(embedded)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.summary()

callbacks = [
    keras.callbacks.ModelCheckpoint("embeddings_bidir_lstm.keras",
                                    save_best_only=True)
]
model.fit(int_train_ds, validation_data=int_val_ds, epochs=10, callbacks=callbacks)
model = keras.models.load_model("embeddings_bidir_lstm.keras")
print(f"테스트 정확도: {model.evaluate(int_test_ds)[1]:.3f}")

테스트 정확도는 87%가 나왔고, LSTM이 20000차원이 아니라 256차원의 벡터를 처리하기 때문에 이 모델은 원-핫 모델보다 훨씬 빠르고 정확도는 비슷하다. 정확도가 제대로 개선이 안된 이유는 모델이 약간 적은 데이터를 사용했기 때문이다. 바이그램 모델은 전체 리뷰를 처리하지만 이 시퀀스 모델은 600개의 단어 이후 시퀀스를 잘라버린다..

이를 개선해보자 !

패딩과 마스킹 이해하기

입력 시퀀스가 0으로 가득 차 있으면 모델의 성능에 나쁜 영향을 미친다. 이는 TextVectorization 층에 output_sequence_length = max_length 옵션을 사용했기 때문이다. 600개의 토큰보다 긴 문장은 600개의 토큰 길이로 잘리고, 적다면 끝에 0으로 채워진다.
RNN층은, 두 RNN층이 병렬로 실행되는 양방향 RNN을 사용하는데, 한 층은 원래 순서대로 토큰을 처리하고 다른 층은 동일한 토큰을 거꾸로 처리한다.

여기서 문제는 !

원래 순서대로 토큰을 바라보는 RNN층은 마지막에 패딩이 인코딩된 벡터만 처리하게 되어, 이 RNN의 내부 상태에 저장된 정보는 이런 의미없는 입력을 처리하면서 점차 사라지게 되는 문제가 발생한다.

RNN층이 이런 패딩을 건너뛰게 만들 방법이 필요하다. 이를 위한 API가 마스킹(masking) 이다.

Embedding층은 입력 데이터에 상응하는 마스킹 을 생성할 수 있다. 이 마스킹은 1과 0으로 이루어진 (batch_size, sequence_length) 크기의 텐서이다.
케라스에서는 마스킹을 처리할 수 있는 모든 층에 자동으로 전달한다. 이 마스킹을 사용하여 RNN 층은 마스킹된 스텝을 건너뛴다. 모델이 전체 시퀀스를 반환한다면 손실 함수도 마스킹을 사용하여 출력 시퀀스에서 마스킹된 스텝을 건너뛸 것이다.

마스킹을 활성화하여 모델을 다시 훈련해보자.

inputs = keras.Input(shape=(None,), dtype="int64")
embedded = layers.Embedding(
    input_dim=max_tokens, output_dim=256, mask_zero=True)(inputs)
x = layers.Bidirectional(layers.LSTM(32))(embedded)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.summary()

callbacks = [
    keras.callbacks.ModelCheckpoint("embeddings_bidir_lstm_with_masking.keras",
                                    save_best_only=True)
]
model.fit(int_train_ds, validation_data=int_val_ds, epochs=10, callbacks=callbacks)
model = keras.models.load_model("embeddings_bidir_lstm_with_masking.keras")
print(f"테스트 정확도: {model.evaluate(int_test_ds)[1]:.3f}")

이번에는 88%로 성능이 향상된 것을 확인할 수 있다.

사전 훈련된 단어 임베딩 사용하기

훈련 데이터가 부족하면 작업에 맞는 단어 임베딩을 학습할 수 없다. 이런 경우에는, 풀려는 문제와 함께 단어 임베딩을 학습하는 대신 미리 계산된 임베딩 공간의 임베딩 벡터를 로드할 수 있다. 또한 다른 문제에서 학습한 특성을 재사용하는 것이 합리적이다.

케라스에서 Embedding층을 위해 내려받을 수 있는 미리 계산된 단어 임베딩 데이터베이스가 여럿 있다. 여기서는 GloVe를 알아보자.

GloVe 단어 임베딩 파일 파싱해보자.

import numpy as np
path_to_glove_file = "glove.6B.100d.txt"

embeddings_index = {}
with open(path_to_glove_file) as f:
    for line in f:
        word, coefs = line.split(maxsplit=1)
        coefs = np.fromstring(coefs, "f", sep=" ")
        embeddings_index[word] = coefs

print(f"단어 벡터 개수: {len(embeddings_index)}")

GloVe 단어 임베딩 행렬 준비해보자.

embedding_dim = 100

vocabulary = text_vectorization.get_vocabulary()
word_index = dict(zip(vocabulary, range(len(vocabulary))))

embedding_matrix = np.zeros((max_tokens, embedding_dim))
for word, i in word_index.items():
    if i < max_tokens:
        embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector
embedding_layer = layers.Embedding(
    max_tokens,
    embedding_dim,
    embeddings_initializer=keras.initializers.Constant(embedding_matrix),
    trainable=False,
    mask_zero=True,
)

마지막으로, 사전 훈련된 임베딩을 사용하는 모델을 작성해보자.

inputs = keras.Input(shape=(None,), dtype="int64")
embedded = embedding_layer(inputs)
x = layers.Bidirectional(layers.LSTM(32))(embedded)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop",
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.summary()

callbacks = [
    keras.callbacks.ModelCheckpoint("glove_embeddings_sequence_model.keras",
                                    save_best_only=True)
]
model.fit(int_train_ds, validation_data=int_val_ds, epochs=10, callbacks=callbacks)
model = keras.models.load_model("glove_embeddings_sequence_model.keras")
print(f"테스트 정확도: {model.evaluate(int_test_ds)[1]:.3f}")

출처 : 케라스 창시자에게 배우는 딥러닝(개정판)

profile
어서오세요.

0개의 댓글