StringLookup 층

텍스트로 된 범주형 특성을 원-핫 인코딩 할 때는 StringLookup 층을 이용한다.

>>> cities = ['Auckland', 'Paris', 'Paris', 'San Francisco']
>>> str_lookup_layer = tf.keras.layers.StringLookup()
>>> str_lookup_layer.adapt(cities)
>>> str_lookup_layer(cities)
>>> str_lookup_layer([['Paris'], ['Auckland'], ['Auckland'], ['Montreal']])
<tf.Tensor: shape=(4, 1), dtype=int64, numpy=
array([[1],
       [3],
       [3],
       [0]])>

원-핫 vector를 출력하고 싶다면 output_mode='one_hot'으로 설정한다.

>>> str_lookup_layer = tf.keras.layers.StringLookup(output_mode='one_hot')
>>> str_lookup_layer.adapt(cities)
>>> str_lookup_layer([['Paris'], ['Auckland'], ['Auckland'], ['Montreal']])
<tf.Tensor: shape=(4, 4), dtype=int64, numpy=
array([[0, 1, 0, 0],
       [0, 0, 0, 1],
       [0, 0, 0, 1],
       [1, 0, 0, 0]])>

Train set이 너무 크면 랜덤하게 일부를 추출해서 adapt() 메서드에 전달하는 것이 편리할 수 있다. 물론 빈도가 적은 범주는 놓칠 수 있다(이런 범주는 모두 0으로 매핑된다). OOV(Out-Of-Vocabulary) 버킷(bucket) 개수를 지정해서 이런 위험을 줄일 수 있다. 알 수 없는 범주는 해시(hash) 함수를 이용해 OOV 버킷 중 하나로 랜덤하게 매핑된다.

>>> str_lookup_layer = tf.keras.layers.StringLookup(num_oov_indices=5)
>>> str_lookup_layer.adapt(cities)
>>> str_lookup_layer([['Paris'], ['Auckland'], ['Foo'], ['Bar'], ['Baz']])
<tf.Tensor: shape=(5, 1), dtype=int64, numpy=
array([[5],
       [7],
       [4],
       [3],
       [4]])>

알 수 없는 범주([‘Foo’], [‘Bar’], [‘Baz’])들이 같거나(해싱 충돌) 다른 OOV 버킷으로 매핑된 것을 확인할 수 있다. 해싱 충돌로 인해 같은 범주로 구분된 이상 더는 둘을 구분할 수 없으며, 이를 해결하는 방법은 OOV 버킷의 개수를 늘리는 것 뿐이다. 하지만 이렇게 하면 애초에 OOV 버킷을 사용하는 의미가 퇴색될 것이다.


Hashing 층

Hashing 층은 범주마다 해시를 계산하고 버킷(구간) 개수로 나눈 나머지를 구한다. 이는 재현 가능한 랜덤(구간의 개수가 바뀌지 않으면 항상 같은 정수로 매핑)이므로 안정적이다.

>>> hashing_layer = tf.keras.layers.Hashing(num_bins=10)
>>> hashing_layer([['Paris'], ['Tokyo'], ['Auckland'], ['Montreal']])
<tf.Tensor: shape=(4, 1), dtype=int64, numpy=
array([[0],
       [1],
       [9],
       [1]])>

adapt() 메서드를 호출할 필요가 없는 것이 장점이다. 물론 여기서도 해싱 충돌(위 예시에서 Tokyo와 Momtreal)은 발생한다.


Embedding

임베딩(embedding)은 범주나 어휘 사전의 단어와 같은 고차원 데이터의 밀집 표현이다. 50,000개의 범주가 있을 때, 원-핫 인코딩은 50,000차원의 희소 벡터를 만들지만 임베딩은 상대적으로 작은(e.g. 100차원) 밀집 벡터이다.

임베딩은 랜덤하게 초기화되며, 경사 하강법으로 훈련될 수 있다. 따라서 비슷한 범주들은 점점 가까워진다. 범주가 유용하게 표현되도록 임베딩이 훈련되는 것을 표현 학습이라고 한다.

keras는 임베딩 행렬을 감싼 Embedding 층을 제공한다. 이 행렬을 범주마다 하나의 행, 임베딩 차원마다 하나의 열을 가진다.

>>> tf.random.set_seed(42)
>>> embedding_layer = tf.keras.layers.Embedding(input_dim=5, output_dim=2)      # 임베딩 층 생성
>>> embedding_layer(np.array([2, 4, 2]))        # 샘플 주입
<tf.Tensor: shape=(3, 2), dtype=float32, numpy=
array([[-0.02826967, -0.04448348],
       [-0.01479431,  0.0248084 ],
       [-0.02826967, -0.04448348]], dtype=float32)>

먼저 5개의 범주와 2개의 차원을 가지는 임베딩 층을 만들었다. 그리고 임베딩 층에 샘플을 넣으니, 그 샘플에 해당하는 행을 찾아 반환하였다.

Embedding 층은 StringLookup 층과 연결하여 사용할 수 있다.


TextVectorization 층 (텍스트 전처리)

TextVectorization 층을 사용하면 텍스트 전처리를 할 수 있다. vocabulary 매개변수로 어휘 사전을 전달하거나, 훈련 데이터와 adapt() 메서드를 함께 사용할 수도 있다.

>>> train_data = ["To be", "!(to be)", "That's the question", "Be, be, be."]
>>> text_vec_layer = tf.keras.layers.TextVectorization()
>>> text_vec_layer.adapt(train_data)
>>> text_vec_layer(["Be good!", "Question: be or be?"])
<tf.Tensor: shape=(2, 4), dtype=int64, numpy=
array([[2, 1, 0, 0],
       [6, 2, 1, 2]])>

TextVectorization 층이 어휘 사전을 만드는 과정은 다음과 같다.

  1. 훈련 데이터를 소문자로 바꾸고 구두점 삭제.
  2. 문장을 공백으로 나눔.
  3. 단어를 빈도에 따라 정렬. 알 수 없는 단어는 1로 인코딩.

1번 과정 때문에 ‘Be’, ‘be’, ‘be?’가 모두 ‘be’=2로 인코딩된 것이다. 결과에 등장하는 0은 단지 문장 길이를 맞추기 위함이다.

하지만 위 방법은 단어 간의 상대적 중요도를 간과한다는 단점이 있다. 예를 들어, the, to 같이 자주 나타나는 단어보다 ‘basketball’과 같이 드물지만 더 많은 정보를 가진 단어가 있을 수 있다. 따라서 멀티-핫 인코딩이나 카운트 인코딩보다는 TF(Term-Frequency) X IDF(Inverse-Document-Frequency)를 사용하는 것이 좋다. TextVectorization 층의 TF-IDF는 단어 카운트에 $\log{\frac{1+d}{(f+1)}}$ 을 가중치로 곱한다(d는 전체 문장의 개수, f는 주어진 단어가 포함된 문장의 개수). 이를 사용하면 등장 빈도까지 고려하여 인코딩할 수 있다.

>>> text_vec_layer = tf.keras.layers.TextVectorization(output_mode='tf_idf')
>>> text_vec_layer.adapt(train_data)
>>> text_vec_layer(["Be good!", "Question: be or be?"])
<tf.Tensor: shape=(2, 6), dtype=float32, numpy=
array([[0.96725637, 0.6931472 , 0.        , 0.        , 0.        ,
        0.        ],
       [0.96725637, 1.3862944 , 0.        , 0.        , 0.        ,
        1.0986123 ]], dtype=float32)>


사전 훈련된 언어 모델 구성 요소 사용하기

앞서 살펴본 TextVectorization 층도 유용하지만 여전히 문제점이 있다. 동음이의어를 구분하지 못하며(형태만 고려하므로), 비슷한 관계의 단어(e.g. evolution과 evolutionary)에 대한 정보도 전혀 고려하지 않는다.

Tensorflow hub 라이브러리를 활용하면 텍스트, 이미지, 오디오 등을 위해 사전 훈련된 모델의 구성 요소를 쉽게 재사용할 수 있다. 다음은 원시 텍스트를 입력 받아 50개 차원 문장 임베딩을 출력하는 nnlm-en-dim50 모듈 버전 2를 사용한 것이다.

>>> import tensorflow_hub as hub
>>> hub_layer = hub.KerasLayer('https://tfhub.dev/google/nnlm-en-dim50/2')
>>> sentence_embeddings = hub_layer(tf.constant(["To be", "Not to be"]))
>>> sentence_embeddings.numpy().round(2)
array([[-0.25,  0.28,  0.01,  0.1 ,  0.14,  0.16,  0.25,  0.02,  0.07,
         0.13, -0.19,  0.06, -0.04, -0.07,  0.  , -0.08, -0.14, -0.16,
         0.02, -0.24,  0.16, -0.16, -0.03,  0.03, -0.14,  0.03, -0.09,
        -0.04, -0.14, -0.19,  0.07,  0.15,  0.18, -0.23, -0.07, -0.08,
         0.01, -0.01,  0.09,  0.14, -0.03,  0.03,  0.08,  0.1 , -0.01,
        -0.03, -0.07, -0.1 ,  0.05,  0.31],
       [-0.2 ,  0.2 , -0.08,  0.02,  0.19,  0.05,  0.22, -0.09,  0.02,
         0.19, -0.02, -0.14, -0.2 , -0.04,  0.01, -0.07, -0.22, -0.1 ,
         0.16, -0.44,  0.31, -0.1 ,  0.23,  0.15, -0.05,  0.15, -0.13,
        -0.04, -0.08, -0.16, -0.1 ,  0.13,  0.13, -0.18, -0.04,  0.03,
        -0.1 , -0.07,  0.07,  0.03, -0.08,  0.02,  0.05,  0.07, -0.14,
        -0.1 , -0.18, -0.13, -0.04,  0.15]], dtype=float32)


이미지 전처리 층

keras 전처리 API에는 3가지의 이미지 전처리 층이 있다.

  • tf.keras.layers.Resizing: 입력 이미지를 원하는 크기로 바꾼다. crop_to_aspect_ratio=True로 설정하면 왜곡을 피하기 위해 목표 이미지 비율에 맞게 이미지를 자른다.
  • tf.keras.layers.Rescaling: 픽셀 값의 스케일을 조정한다.
  • tf.keras.layers.CenterCrop: 원하는 높이와 너비의 중간 부분만 유지하면서 이미지를 자른다.

다음은 tf.keras.layers.CenterCrop을 활용한 예시이다.

from sklearn.datasets import load_sample_image

images = load_sample_image(image_name='china.jpg')
crop_image_layer = tf.keras.layers.CenterCrop(height=100, width=100)
cropped_images = crop_image_layer(images)



Tensorflow dataset project

Tensorflow Dataset(TFDS)을 사용하면 널리 사용되는 데이터셋을 손쉽게 다운받을 수 있다.

다음은 TFDS에서 MNIST 데이터를 다운받고 train set의 첫 90%를 훈련에, 나머지 10%는 검증에, test set은 전부 테스트에 사용하여 훈련하는 과정을 담은 코드이다.

train_set, valid_set, test_set = tfds.load(
    name="mnist",
    split=["train[:90%]", "train[90%:]", "test"],
    as_supervised=True
)
train_set = train_set.shuffle(10_000, seed=42).batch(32).prefetch(1)
valid_set = valid_set.batch(32).cache()
test_set = test_set.batch(32).cache()
tf.random.set_seed(42)
model = tf.keras.Sequential([
    tf.keras.layers.Flatten(input_shape=(28, 28)),
    tf.keras.layers.Dense(10, activation="softmax")
])
model.compile(loss="sparse_categorical_crossentropy", optimizer="nadam",
              metrics=["accuracy"])
history = model.fit(train_set, validation_data=valid_set, epochs=5)
test_loss, test_accuracy = model.evaluate(test_set)


코드 보러가기



별도의 출처 표시가 있는 이미지를 제외한 모든 이미지는 강의자료에서 발췌하였음을 밝힙니다.

댓글남기기