감사말: 프랑소와 숄레의 Deep Learning with Python, Second Edition 10장에 사용된 코드에 대한 설명을 담고 있으며 텐서플로우 2.6 버전에서 작성되었습니다. 소스코드를 공개한 저자에게 감사드립니다.
tensorflow 버전과 GPU 확인
구글 코랩 설정: '런타임 -> 런타임 유형 변경' 메뉴에서 GPU 지정 후 아래 명령어 실행 결과 확인
!nvidia-smi
사용되는 tensorflow 버전 확인
import tensorflow as tf
tf.__version__
tensorflow가 GPU를 사용하는지 여부 확인
tf.config.list_physical_devices('GPU')
파이썬, 자바, C, C++, C#, 자바스크립트 등 컴퓨터 프로그래밍언어와 구분하기 위해 일상에서 사용되는 한국어, 영어 등을 자연어(natural language)라 부른다.
자연어의 특성상 정확한 분석을 위한 알고리즘을 구현하는 일은 사실상 매우 어렵다. 딥러닝 기법이 활용되기 이전깢지 적절한 규칙을 구성하여 자연어를 이해하려는 수 많은 시도가 있어왔지만 별로 성공적이지 않았다.
1990년대부터 인터넷으로부터 구해진 엄청난 양의 텍스트 데이터에 머신러닝 기법을 적용하기 시작했다. 단, 주요 목적이 언어의 이해가 아니라 아래 예제들처럼 입력 텍스트를 분석하여 통계적으로 유용한 정보를 예측하는 방향으로 수정되었다.
이와 같은 분석을 자연어처리(NLP, Natural Language Processing)이라 하며 단어(words), 문장(sentences), 문단(paragraphs) 등에서 찾을 수 있는 패턴(pattern)을 인식하려 시도한다.
머신러닝 활용
자연어처리를 위해 1990년대부터 시작된 머신러닝 활용의 변화과정은 다음과 같다.
1990 - 2010년대 초반: 결정트리(decision trees), 로지스틱 회귀(logistic regression) 모델이 주로 활용됨.
2014-2015: LSTM 등 시퀀스 처리 알고리즘 활용 시작
2015-2017: (양방향) 순환신경망이 기본적으로 활용됨.
2017-2018: 트랜스포머(Transformer) 모델이 최고의 성능 발휘하며, 많은 난제들을 해결함. 현재 가장 많이 활용되는 모델임.
딥러닝 모델은 텍스트 자체를 처리할 수 없다. 따라서 택스트를 수치형 텐서(numeric tensors)로 변환하는 텍스트 벡터화(text vectorization) 과정이 요구되며 보통 다음 세 단계를 따른다.
아래 그림은 텍스트 벡터화의 기본적인 과정을 잘 보여준다.
텍스트 표준화
다음 두 문장을 표준화를 통해 동일한 문장으로 변환해보자.
예를 들어 다음 표준화 기법을 사용할 수 있다.
.
, ;
, ?
, '
등 특수 기호 제거그러면 위 두 문장 모두 아래 문장으로 변환된다.
표준화 과정을 통해 어느 정도의 정보를 상실하게 되지만
학습해야할 내용을 줄여 일반화 성능이 보다 좋은 모델을 훈련시키는 장점이 있다.
하지만 분석 목적에 따라 표준화 기법은 경우에 따라 달라질 수 있음에 주의해야 한다.
예를 들어 인터뷰 기사의 경우 물음표(?
)는 제거하면 안된다.
토큰화
텍스트 표준화 이후 데이터 분석의 기본 단위인 토큰으로 쪼개야 한다. 보통 아래 세 가지 방식 중에 하나를 사용한다.
일반적으로 문자 기준 토큰화는 잘 사용되지 않는다. 여기서도 단어 기준 또는 N-그램 토큰화만 이용한다.
단어주머니(bag-of-words)는 N-토큰으로 구성된 집합을 의미하며 N-그램 주머니(bag-of-N-grams)라고 불리기도 한다. 예를 들어 "the cat sat on the mat." 문장에 대한 2-그램 집합과 3-그램 집합은 각각 다음과 같다.
{"the", "the cat", "cat", "cat sat", "sat",
"sat on", "on", "on the", "the mat", "mat"}
{"the", "the cat", "cat", "cat sat", "the cat sat",
"sat", "sat on", "on", "cat sat on", "on the",
"sat on the", "the mat", "mat", "on the mat"}
어휘 색인화
일반적으로 먼저 훈련셋에 포함된 모든 토큰들의 색인(인덱스)을 작성한다. 생성된 색인을 각 토큰을 바탕으로 원-핫, 멀티-핫 인코딩 등의 방식을 사용하여 수치형 텐서로 변환한다.
4장과
5장에서
설명한 대로 보통 사용 빈도수가 높은 2만 또는 3만 개의 단어만을 대상으로 어휘 색인화를 진행한다.
당시에 num_words=10000
을 사용하여 사용 빈도수가 상위 1만 등 안에 드는 단어만을
대상으로 훈련셋을 구성하였다.
from tensorflow.keras.datasets import imdb
(train_data, train_labels), (test_data, test_labels) = imdb.load_data(num_words=10000)
케라스의 imdb 데이터셋은 이미 정수들의 시퀀스로 전처리가 되어 있다. 하지만 여기서는 원본 imdb 데이터셋을 대상으로 전처리를 직접 수행하는 단계부터 살펴볼 것이다. 이를 위해 아래 사항을 기억해 두어야 한다.
마스크(mask) 토큰: 무신되어야 하는 토큰을 나타냄. 모두 0으로 처리.
예를 들어, 문장의 길이를 맞추기 위해 사용되는 패딩으로 0으로 채워줄 수 있음.
[[5, 7, 124, 4, 89]
[8, 34, 21, 0, 0]]
케라스의 TextVectorization
층 활용
지금까지 설명한 텍스트 벡터화를 위해 케라스의 TextVectorization
층을 활용할 수 있으며
기본 사용법은 다음과 같다.
from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
output_mode="int",
)
TextVectorization
층 구성에 사용되는 주요 기본 설정은 다음과 같다.
standardize='lower_and_strip_punctuation'
ngrams=None
split='whitespace'
output_mode="int"
표준화와 토큰화 방식을 임의로 지정해서 활용할 수도 있다.
다만, 파이썬의 기본 문자열 자료형인 str
대신에 tf.string
텐서를 활용해야 함에 주의해야 한다.
표준화와 토큰화의 기본값은 아래 두 함수를 활용하는 것과 동일하다.
custom_standardization_fn()
custom_split_fn()
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 = [
"I write, erase, rewrite",
"Erase again, and then",
"A poppy blooms.",
]
text_vectorization.adapt(dataset)
생성된 어휘 색인은 다음과 같다.
vocabulary = text_vectorization.get_vocabulary()
vocabulary
['', '[UNK]', 'erase', 'write', 'then', 'rewrite', 'poppy', 'i', 'blooms', 'and', 'again', 'a']
생성된 어휘 색인을 활용하여 새로운 문장을 벡터화 해보자.
test_sentence = "I write, rewrite, and still rewrite again"
encoded_sentence = text_vectorization(test_sentence)
print(encoded_sentence)
tf.Tensor([ 7 3 5 9 1 5 10], shape=(7,), dtype=int64)
벡터화된 텐서로부터 문장을 복원하면 표준화된 문장이 생성된다.
inverse_vocab = dict(enumerate(vocabulary))
decoded_sentence = " ".join(inverse_vocab[int(i)] for i in encoded_sentence)
print(decoded_sentence)
i write rewrite and [UNK] rewrite again
TextVectorization
층 사용법
TextVectorization
층은 GPU 또는 TPU에서 지원되지 않는다.
따라서 모델 구성에 직접 사용하는 방식은 모델의 훈련을
늦출 수 있기에 권장되지 않는다.
여기서는 대신에 데이터셋 전처리를 모델 구성과 독립적으로 처리하는 방식을 이용한다.
하지만 훈련이 완성된 모델을 실전에 배치할 경우 TextVectorization
층을
완성된 모델에 추가해서 사용하는 게 좋다.
이에 대한 자세한 설명은 잠시 뒤에 부록에서 설명한다.
앞서 언급한 대로 자연어처리 모델에 따라 단어 모음을 다루는 방식이 다르다.
여기서는 IMDB 영화 리뷰 데이터를 이용하여 두 모델 방식의 활용법과 차이점을 소개한다.
이전과는 달리 여기서는 IMDB 데이터셋을 직접 다운로드하여 전처리하는 과정을 살펴본다.
준비 과정 1: 데이터셋 다운로드 압축 풀기
압축을 풀면 아래 구조의 디렉토리가 생성된다.
aclImdb/
...train/
......pos/
......neg/
...test/
......pos/
......neg/
train
의 pos
와 neg
서브디렉토리에 각각 12,500개의 긍정과 부정 리뷰가
포함되어 있다.
주의사항: 아래 코드는 윈도우의 경우 10 최신 버전 또는 11부터 지원된다.
!curl -O https://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
!tar -xf aclImdb_v1.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 80.2M 100 80.2M 0 0 26.1M 0 0:00:03 0:00:03 --:--:-- 26.1M
aclImdb/train/unsup
서브디렉토리는 필요 없기에 삭제한다.
if 'google.colab' in str(get_ipython()):
!rm -r aclImdb/train/unsup
else:
import shutil
unsup_path = './aclImdb/train/unsup'
shutil.rmtree(unsup_path)
긍정 리뷰 하나의 내용을 살펴보자. 모델 구성 이전에 훈련 데이터셋을 살펴 보고 모델에 대한 직관을 갖는 과정이 항상 필요하다.
if 'google.colab' in str(get_ipython()):
!cat aclImdb/train/pos/4077_10.txt
else:
with open('aclImdb/train/pos/4077_10.txt', 'r') as f:
text = f.read()
print(text)
I first saw this back in the early 90s on UK TV, i did like it then but i missed the chance to tape it, many years passed but the film always stuck with me and i lost hope of seeing it TV again, the main thing that stuck with me was the end, the hole castle part really touched me, its easy to watch, has a great story, great music, the list goes on and on, its OK me saying how good it is but everyone will take there own best bits away with them once they have seen it, yes the animation is top notch and beautiful to watch, it does show its age in a very few parts but that has now become part of it beauty, i am so glad it has came out on DVD as it is one of my top 10 films of all time. Buy it or rent it just see it, best viewing is at night alone with drink and food in reach so you don't have to stop the film.<br /><br />Enjoy
준비 과정 2: 검증셋 준비
훈련셋의 20%를 검증셋으로 떼어낸다.
이를 위해 aclImdb/val
디렉토리를 생성한 후에
긍정과 부정 훈련셋 모두 무작위로 섞은 후 그중 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) # val 디렉토리 생성
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)
준비 과정 3: 텐서 데이터셋 준비
text_dataset_from_directory()
함수를 이용하여
훈련셋, 검증셋, 테스트셋을 준비한다.
자료형은 모두 Dataset
이며, 배치 크기는 32를 사용한다.
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
)
Found 20000 files belonging to 2 classes. Found 5000 files belonging to 2 classes. Found 25000 files belonging to 2 classes.
각 데이터셋은 배치로 구분되며
입력은 tf.string
텐서이고, 타깃은 int32
텐서이다.
크기는 모두 32이며 지정된 배치 크기이다.
예를 들어, 첫째 배치의 입력과 타깃 데이터의 정보는 다음과 같다.
for inputs, targets in train_ds:
print("inputs.shape:", inputs.shape)
print("inputs.dtype:", inputs.dtype)
print("targets.shape:", targets.shape)
print("targets.dtype:", targets.dtype)
# 예제: 첫째 배치의 첫째 리뷰
print("inputs[0]:", inputs[0])
print("targets[0]:", targets[0])
break
inputs.shape: (32,) inputs.dtype: <dtype: 'string'> targets.shape: (32,) targets.dtype: <dtype: 'int32'> inputs[0]: tf.Tensor(b'The film begins with a bunch of kids in reform school and focuses on a kid named \'Gabe\', who has apparently worked hard to earn his parole. Gabe and his sister move to a new neighborhood to make a fresh start and soon Gabe meets up with the Dead End Kids. The Kids in this film are little punks, but they are much less antisocial than they\'d been in other previous films and down deep, they are well-meaning punks. However, in this neighborhood there are also some criminals who are perpetrating insurance fraud through arson and see Gabe as a convenient scapegoat--after all, he\'d been to reform school and no one would believe he was innocent once he was framed. So, when Gabe is about ready to be sent back to "The Big House", it\'s up to the rest of the gang to save him and expose the real crooks.<br /><br />The "Dead End Kids" appeared in several Warner Brothers films in the late 1930s and the films were generally very good (particularly ANGELS WITH DIRTY FACES). However, after the boys\' contracts expired, they went on to Monogram Studios and the films, to put it charitably, were very weak and formulaic--with Huntz Hall and Leo Gorcey being pretty much the whole show and the group being renamed "The Bowery Boys". Because ANGELS WASH THEIR FACES had the excellent writing and production values AND Hall and Gorcey were not constantly mugging for the camera, it\'s a pretty good film--and almost earns a score of 7 (it\'s REAL close). In fact, while this isn\'t a great film aesthetically, it\'s sure a lot of fun to watch, so I will give it a 7! Sure, it was a tad hokey-particularly towards the end when the kids take the law into their own hands and Reagan ignores the Bill of Rights--but it was also quite entertaining. The Dead End Kids are doing their best performances and Ronald Reagan and Ann Sheridan provided excellent support. Sure, this part of the film was illogical and impossible but somehow it was still funny and rather charming--so if you can suspend disbelief, it works well.', shape=(), dtype=string) targets[0]: tf.Tensor(1, shape=(), dtype=int32)
단어주머니에 채울 토큰으로 어떤 N-그램을 사용할지 먼저 지정해야 한다.
방식 1: 유니그램 바이너리 인코딩
예를 들어 "the cat sat on the mat" 문장을 유니그램으로 처리하면 다음 단어주머니가 생성된다. 집합으로 처리되기에 단어들의 순서는 완전히 무시된다.
{"cat", "mat", "on", "sat", "the"}
이제 모든 문장은 어휘색인에 포함된 단어들의 수만큼 긴 1차원 이진 텐서(binary tensor)로 처리된다. 즉, 멀티-핫(multi-hot) 인코딩 방식을 사용해서 텐서로 변환된다. 4장과 5장에서 문장을 인코딩 방식과 동일하다.
TextVectorization
클래스의 output_mode="multi_hot"
옵션을 이용하면
방금 설명한 내용을 그대로 처리해준다.
from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
max_tokens=20000,
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))
binary_1gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
binary_1gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))
변환된 첫째 배치의 입력과 타깃 데이터의 정보는 다음과 같다.
max_tokens=20000
으로 지정하였기에 모든 문장은 길이가 2만인 벡터로 변환되었다.
for inputs, targets in binary_1gram_train_ds:
print("inputs.shape:", inputs.shape)
print("inputs.dtype:", inputs.dtype)
print("targets.shape:", targets.shape)
print("targets.dtype:", targets.dtype)
print("inputs[0]:", inputs[0])
print("targets[0]:", targets[0])
break
inputs.shape: (32, 20000) inputs.dtype: <dtype: 'float32'> targets.shape: (32,) targets.dtype: <dtype: 'int32'> inputs[0]: tf.Tensor([1. 1. 1. ... 0. 0. 0.], shape=(20000,), dtype=float32) targets[0]: tf.Tensor(0, shape=(), dtype=int32)
밀집 모델 지정
단어주머니 모델로 여기서는 밀집 모델을 사용한다.
get_model()
함수가 컴파일 된 단순한 밀집 모델을 반환한다.
모델의 출력값은 긍정일 확률이며,
최상위 층의 활성화 함수로 sigmoid
를 사용한다.
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()
Model: "model" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 20000)] 0 dense (Dense) (None, 16) 320016 dropout (Dropout) (None, 16) 0 dense_1 (Dense) (None, 1) 17 ================================================================= Total params: 320,033 Trainable params: 320,033 Non-trainable params: 0 _________________________________________________________________
모델 훈련
밀집 모델 훈련과정은 특별한 게 없다. 훈련 후 테스트셋에 대한 정확도가 89% 보다 조금 낮게 나온다. 최고 성능의 모델이 테스트셋에 대해 95% 정도 정확도를 내는 것보다는 낮지만 무작위로 찍는 모델보다는 훨씬 좋은 모델이다.
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(),
epochs=10,
callbacks=callbacks)
model = keras.models.load_model("binary_1gram.keras")
print(f"Test acc: {model.evaluate(binary_1gram_test_ds)[1]:.3f}")
Epoch 1/10 625/625 [==============================] - 10s 16ms/step - loss: 0.4074 - accuracy: 0.8277 - val_loss: 0.2792 - val_accuracy: 0.8908 Epoch 2/10 625/625 [==============================] - 3s 5ms/step - loss: 0.2746 - accuracy: 0.8981 - val_loss: 0.2774 - val_accuracy: 0.8964 Epoch 3/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2471 - accuracy: 0.9115 - val_loss: 0.2872 - val_accuracy: 0.8976 Epoch 4/10 625/625 [==============================] - 3s 5ms/step - loss: 0.2246 - accuracy: 0.9244 - val_loss: 0.3187 - val_accuracy: 0.8936 Epoch 5/10 625/625 [==============================] - 3s 6ms/step - loss: 0.2156 - accuracy: 0.9313 - val_loss: 0.3164 - val_accuracy: 0.8960 Epoch 6/10 625/625 [==============================] - 3s 6ms/step - loss: 0.2108 - accuracy: 0.9341 - val_loss: 0.3355 - val_accuracy: 0.8934 Epoch 7/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2052 - accuracy: 0.9366 - val_loss: 0.3354 - val_accuracy: 0.8944 Epoch 8/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2017 - accuracy: 0.9365 - val_loss: 0.3582 - val_accuracy: 0.8940 Epoch 9/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2013 - accuracy: 0.9394 - val_loss: 0.3497 - val_accuracy: 0.8938 Epoch 10/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2043 - accuracy: 0.9394 - val_loss: 0.3631 - val_accuracy: 0.8940 782/782 [==============================] - 8s 10ms/step - loss: 0.2861 - accuracy: 0.8885 Test acc: 0.888
방식 2: 바이그램 바이너리 인코딩
바이그램(2-grams)을 유니그램 대신 이용해보자. 예를 들어 "the cat sat on the mat" 문장을 바이그램으로 처리하면 다음 단어주머니가 생성된다.
{"the", "the cat", "cat", "cat sat", "sat",
"sat on", "on", "on the", "the mat", "mat"}
TextVectorization
클래스의 ngrams=N
옵션을 이용하면
N-그램들로 이루어진 어휘색인을 생성할 수 있다.
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))
binary_2gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
binary_2gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))
훈련 후 테스트셋에 대한 정확도가 90%를 조금 웃돌 정도로 많이 향상되었다.
model = get_model()
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"Test acc: {model.evaluate(binary_2gram_test_ds)[1]:.3f}")
Epoch 1/10 625/625 [==============================] - 12s 18ms/step - loss: 0.3857 - accuracy: 0.8347 - val_loss: 0.2791 - val_accuracy: 0.9000 Epoch 2/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2592 - accuracy: 0.9082 - val_loss: 0.2947 - val_accuracy: 0.8988 Epoch 3/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2277 - accuracy: 0.9241 - val_loss: 0.3060 - val_accuracy: 0.8978 Epoch 4/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2074 - accuracy: 0.9333 - val_loss: 0.3417 - val_accuracy: 0.8994 Epoch 5/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2070 - accuracy: 0.9365 - val_loss: 0.3538 - val_accuracy: 0.8968 Epoch 6/10 625/625 [==============================] - 4s 6ms/step - loss: 0.1997 - accuracy: 0.9395 - val_loss: 0.3908 - val_accuracy: 0.8946 Epoch 7/10 625/625 [==============================] - 4s 6ms/step - loss: 0.1940 - accuracy: 0.9421 - val_loss: 0.3715 - val_accuracy: 0.8940 Epoch 8/10 625/625 [==============================] - 4s 6ms/step - loss: 0.1902 - accuracy: 0.9427 - val_loss: 0.4054 - val_accuracy: 0.8930 Epoch 9/10 625/625 [==============================] - 4s 6ms/step - loss: 0.1952 - accuracy: 0.9432 - val_loss: 0.3848 - val_accuracy: 0.8880 Epoch 10/10 625/625 [==============================] - 4s 6ms/step - loss: 0.1949 - accuracy: 0.9441 - val_loss: 0.4011 - val_accuracy: 0.8912 782/782 [==============================] - 9s 11ms/step - loss: 0.2788 - accuracy: 0.8953 Test acc: 0.895
방식 3: 바이그램 TF-IDF 인코딩
N-그램을 벡터화할 때 사용 빈도를 함께 저장하는 방식을 사용할 수 있다.
단어의 사용 빈도가 아무래도 문장 평가에 중요한 역할을 수행할 것이기 때문이다.
아래 코드에서처럼 output_mode="count"
옵션을 사용하면 된다.
text_vectorization = TextVectorization(
ngrams=2,
max_tokens=20000,
output_mode="count"
)
그런데 이렇게 하면 "the", "a", "is", "are" 등의 사용 빈도는 매우 높은 반면에
"Chollet" 등의 단어는 빈도가 거의 0에 가깝게 나온다.
또한 생성된 벡터의 대부분은 0으로 채워질 것이다.
max_tokens=20000
을 사용한 반면에 하나의 문장엔 많아야 몇 십개 정도의 단어만 사용되었기 때문이다.
inputs[0]: tf.Tensor([1. 1. 1. ... 0. 0. 0.], shape=(20000,), dtype=float32)
이 점을 고려해서 사용 빈도를 정규화한다. 평균을 원점으로 만들지는 않고 TF-IDF 값으로 나누기만 실행한다. 이유는 평균을 옮기면 벡터의 대부분의 값이 0이 아니게 되어 훈련에 보다 많은 계산이 요구되기 때문이다.
TF-IDF의 의미는 다음과 같다.
TF
(Term Frequency)IDF
(Inverse Document Frequency)IDF
값은 높지만 별로 중요하지 않음.TF-IDF = TF / IDF
output_mode="tf_idf"
옵션을 사용하면 TF-IDF 인코딩을 지원한다.
text_vectorization = TextVectorization(
ngrams=2,
max_tokens=20000,
output_mode="tf_idf",
)
훈련 후 테스트셋에 대한 정확도가 다시 89% 아래로 내려간다. 여기서는 별 도움이 되지 않았지만 많은 텍스트 분류 모델에서는 1% 정도의 성능 향상을 가져온다.
주의사항: 아래 코드는 현재(Tensorflow 2.6과 2.7) GPU를 사용하지 않는 경우에만 작동한다. 이유는 아직 모른다(여기 참조).
text_vectorization.adapt(text_only_train_ds)
tfidf_2gram_train_ds = train_ds.map(lambda x, y: (text_vectorization(x), y))
tfidf_2gram_val_ds = val_ds.map(lambda x, y: (text_vectorization(x), y))
tfidf_2gram_test_ds = test_ds.map(lambda x, y: (text_vectorization(x), y))
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"Test acc: {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 [==============================] - 11s 17ms/step - loss: 0.5232 - accuracy: 0.7588 - val_loss: 0.3197 - val_accuracy: 0.8806 Epoch 2/10 625/625 [==============================] - 4s 6ms/step - loss: 0.3534 - accuracy: 0.8442 - val_loss: 0.2946 - val_accuracy: 0.8954 Epoch 3/10 625/625 [==============================] - 4s 6ms/step - loss: 0.3231 - accuracy: 0.8609 - val_loss: 0.3086 - val_accuracy: 0.8864 Epoch 4/10 625/625 [==============================] - 4s 6ms/step - loss: 0.3053 - accuracy: 0.8734 - val_loss: 0.3087 - val_accuracy: 0.8814 Epoch 5/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2781 - accuracy: 0.8845 - val_loss: 0.3225 - val_accuracy: 0.8878 Epoch 6/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2703 - accuracy: 0.8870 - val_loss: 0.3472 - val_accuracy: 0.8702 Epoch 7/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2695 - accuracy: 0.8883 - val_loss: 0.3357 - val_accuracy: 0.8682 Epoch 8/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2650 - accuracy: 0.8931 - val_loss: 0.3343 - val_accuracy: 0.8664 Epoch 9/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2606 - accuracy: 0.8901 - val_loss: 0.3546 - val_accuracy: 0.8580 Epoch 10/10 625/625 [==============================] - 4s 6ms/step - loss: 0.2575 - accuracy: 0.8924 - val_loss: 0.3318 - val_accuracy: 0.8760 782/782 [==============================] - 8s 10ms/step - loss: 0.2998 - accuracy: 0.8927 Test acc: 0.893
부록: 문자열 벡터화 전처리를 함께 처리하는 모델 내보내기
훈련된 모델을 실전에 배치하려면 텍스트 벡터화도 모델과 함께 내보내야 한다.
이를 위해 TextVectorization
층의 결과를 재활용만 하면 된다.
inputs = keras.Input(shape=(1,), dtype="string")
# 텍스트 벡터화 추가
processed_inputs = text_vectorization(inputs)
# 훈련된 모델에 적용
outputs = model(processed_inputs)
# 최종 모델
inference_model = keras.Model(inputs, outputs)
inference_model
은 일반 텍스트 문장을 직접 인자로 받을 수 있다.
예를 들어 "That was an excellent movie, I loved it."라는 리뷰는
긍정일 확률이 매우 높다고 예측된다.
import tensorflow as tf
raw_text_data = tf.convert_to_tensor([
["That was an excellent movie, I loved it."],
])
predictions = inference_model(raw_text_data)
print(f"{float(predictions[0] * 100):.2f} percent positive")
92.10 percent positive