이 글은 다음 예제를 번역한 것입니다.


Introduction

이 예제는 JPEG 파일을 직접 저장하고, 미리 학습된 모델을 사용하지 않으면서 이미지 분류를 어떻게 행하는지 알아보는 예제입니다. 'Cats vs Dogs' 데이터셋을 케라스를 활용하여 분류해봅니다.

데이터셋 생성을 위해 image_dataset_from_directory를 사용하고, 표준화와 augmentation을 위해 Keras Preprocessing Layer를 사용해봅니다.

Setup

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

 

Load the data: the Cats vs Dogs dataset

Raw data download

먼저, 765M 크기의 데이터를 다운로드합니다.
예제와 다르게 그냥 쥬피터 노트북 상에서 zip파일을 다운받고 싶으면, 아래 코드를 활용하면 됩니다.

import requests

target_path = 'datasets.zip'

url = 'https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_3367a.zip'
response = requests.get(url, stream = True)
handle = open(target_path, 'wb')

for chunk in response.iter_content(chunk_size = 512):
    if chunk:
        handle.write(chunk)
        
handle.close()   

압축을 풀었다면, 최상위 폴더에는 Cat, Dog 하위 폴더가 존재합니다.

Filter out corrupted images

손상된 이미지는 어디에나 존재합니다. 여기서는 "JFIF" 헤더를 가진 이미지를 필터링해보겠습니다.
필터링 시, 해당 헤더가 아니면 손상된 이미지이므로 제거합니다.

import os

num_skipped = 0
for folder_name in ("Cat", "Dog"):
    folder_path = os.path.join("PetImages", folder_name)
    for fname in os.listdir(folder_path):
        fpath = os.path.join(folder_path, fname) # PetImages/Cat/63.jpg
        try:
            fobj = open(fpath, "rb")
            # fobj.peek(10)은 10바이트를 가져오는데, 여기서는 보통
            # 헤더를 읽기 위해 전체를 가져온다고 생각해도 무방합니다.
            is_jfif = tf.compat.as_bytes("JFIF") in fobj.peek(10)
        finally:
            fobj.close()

        if not is_jfif:
            num_skipped += 1
            # Delete corrupted image
            os.remove(fpath)

print("Deleted %d images" % num_skipped)

 

Generate a Dataset

image_size = (180, 180)
batch_size = 32

train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    "PetImages",
    validation_split=0.2,
    subset="training",
    seed=1337,
    image_size=image_size,
    batch_size=batch_size,
)
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    "PetImages",
    validation_split=0.2,
    subset="validation",
    seed=1337,
    image_size=image_size,
    batch_size=batch_size,
)

Visualize the data

9개 이미지를 그려봅니다. '1'은 Dog이고, '0'은 Cat에 해당합니다.

import matplotlib.pyplot as plt

# figure 크기를 조절합니다.
plt.figure(figsize=(10, 10))

# 배치 하나를 가져옵니다.
for images, labels in train_ds.take(1):
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(images[i].numpy().astype("uint8"))
        plt.title(int(labels[i]))
        plt.axis("off")

Using Image data augmentation

데이터가 다양하지 않거나, 데이터 개수가 충분하지 않을때 랜덤으로 회전시키거나 fliping하는 등 augmentation을 활용하면 이를 해결할 수 있습니다. 이는 모델이 다양한 데이터를 볼 수 있게 도와줌과 동시에 이를 통해 Overfitting을 (어느정도) 극복할 수 있습니다.

data_augmentation = keras.Sequential(
    [
        layers.experimental.preprocessing.RandomFlip("horizontal"),
        layers.experimental.preprocessing.RandomRotation(0.1),
    ]
)

data_augmentation을 통해 변형된 이미지를 그려봅니다.

plt.figure(figsize=(10, 10))
for images, _ in train_ds.take(1):
    for i in range(9):
        augmented_images = data_augmentation(images)
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(augmented_images[0].numpy().astype("uint8"))
        plt.axis("off")

 

Standardizing the data

현재 우리가 사용할 이미지는 픽셀 type float32, 크기는 180x180으로 이미 어느정도 표준화가 되어 있습니다. 하지만 RGB channel의 값들이 [0, 255] 값을 가지고 있어, 신경망을 사용하기엔 적합하지 않습니다. 따라서 Rescaling layer를 활용해서 [0, 1] 범위로 변환해줄 것입니다.

 

Two options to preprocess the data

augmentation을 적용할 수 있는 두 가지 방법이 있습니다.

>> Option 1: Make it part of the model like this:

inputs = keras.Input(shape=input_shape)
x = data_augmentation(inputs)
x = layers.experimental.preprocessing.Rescaling(1./255)(x)
...  # Rest of the model

이 방법은 모델 실행에서 동시에 사용될 수 있기 때문에 GPU를 사용한다는 큰 장점을 활용할 수 있습니다.

이 방법은 test time에는 동작하지 않기 때문에, evaluate(), predict()에서는 활용되지 않고, 오로지 fit() 동안에만 사용됩니다. 또한, GPU를 보유하고 있다면 이 방법을 긍정적으로 고려해볼 수 있습니다.

>> Option 2: apply it to the dataset, so as to obtain a dataset that yields batches of augmented images, like this:

augmented_train_ds = train_ds.map(
  lambda x, y: (data_augmentation(x, training=True), y))

이 방법은 CPU에서 주로 사용되고, 버퍼를 통해 모델에 입력됩니다.

CPU를 활용하여 학습하는 경우라면 이 방법이 더 효과적입니다.

 

Configure the dataset for performance

prefetch 옵션을 활용하면 더욱 효율적으로 데이터를 생성하여 모델에 입력할 수 있습니다.

train_ds = train_ds.prefetch(buffer_size=32)
val_ds = val_ds.prefetch(buffer_size=32)

 

Build a model

Xception의 small version을 작성해보겠습니다. 이 모델에 대해서는 최적화를 진행하지 않을 것이고, 만약 최적화에 관심이 있다면 Keras Tuner를 고려해보세요.

강조 사항:

  • data_augmentation을 사용하고, 추가로 Rescaling layer를 활용합니다.
  • 마지막 classification layer에 Dropout을 추가합니다.
def make_model(input_shape, num_classes):
    inputs = keras.Input(shape=input_shape)
    # 본격적으로 모델에 입력하기 전에 여기서 augmentation이 진행됩니다.
    # inference time에는 동작하지 않습니다.
    x = data_augmentation(inputs)

    # [0, 1] 변환을 위해 Rescaling Layer를 활용합니다.
    x = layers.experimental.preprocessing.Rescaling(1.0 / 255)(x)
    x = layers.Conv2D(32, 3, strides=2, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    x = layers.Conv2D(64, 3, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    previous_block_activation = x  # Set aside residual

    for size in [128, 256, 512, 728]:
        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(size, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.Activation("relu")(x)
        x = layers.SeparableConv2D(size, 3, padding="same")(x)
        x = layers.BatchNormalization()(x)

        x = layers.MaxPooling2D(3, strides=2, padding="same")(x)

        # Project residual
        residual = layers.Conv2D(size, 1, strides=2, padding="same")(
            previous_block_activation)
        x = layers.add([x, residual])  # Add back residual
        previous_block_activation = x  # Set aside next residual

    x = layers.SeparableConv2D(1024, 3, padding="same")(x)
    x = layers.BatchNormalization()(x)
    x = layers.Activation("relu")(x)

    x = layers.GlobalAveragePooling2D()(x)
    if num_classes == 2:
        activation = "sigmoid"
        units = 1
    else:
        activation = "softmax"
        units = num_classes

    x = layers.Dropout(0.5)(x)
    outputs = layers.Dense(units, activation=activation)(x)
    
    return keras.Model(inputs, outputs)

# image size에 (3, ) 채널을 추가합니다.
model = make_model(input_shape=image_size + (3,), num_classes=2)

 

Train the model

epochs = 50

callbacks = [
    keras.callbacks.ModelCheckpoint("save_at_{epoch}.h5"),
]
model.compile(optimizer=keras.optimizers.Adam(1e-3),
              loss="binary_crossentropy",
              metrics=["accuracy"])
model.fit(train_ds, epochs=epochs, 
          callbacks=callbacks, validation_data=val_ds)

학습을 진행해보세요.

 

Run inference on new data

inference time에는 Dropout과 data augmentation이 비활성화 상태가 됩니다.
하지만 Rescaling layer는 그대로 활용되기 때문에 테스트용 데이터로 추론을 진행할 때 올바른 결과를 얻을 수 있습니다.

img = keras.preprocessing.image.load_img("PetImages/Cat/6779.jpg", 
                                         target_size=image_size)
img_array = keras.preprocessing.image.img_to_array(img)
img_array = tf.expand_dims(img_array, 0)  # Create batch axis

predictions = model.predict(img_array)
score = predictions[0]
print(
    "This image is %.2f percent cat and %.2f percent dog."
    % (100 * (1 - score), 100 * score)
)

신경망 기술은 모든 업계에 큰 변화를 가져다 주고 있습니다.

우리 실생활에 직접적으로 맞닿아있는 금융업계에서도 다양한 변화가 일어나고 있는 것또한 자명한 사실입니다.

내부적으로 리얼(real)한 기술이 적용되었는지는 까봐야 알 수 있지만, 챗봇(Chatbot), 금융 사기 탐지(Fraud Detection), 대출, 보험과 같은 다양한 상품을 초개인화로 제공해주기 위한 추천 시스템(Recommendation System) 기술 등 딥러닝이 화두가 되기 이전에도 다양한 알고리즘으로 문제를 해결하고 있던 것들이 이제금 딥러닝 기술 적용으로 새로운 성능을 맛보고 있습니다. 심지어 이제 많은 사람들이 당연하고, 편하게 이용하고 있는 음성/얼굴 인식, 생체 인식 기술도 딥러닝 기술에 포함됩니다.

  • 딥러닝으로 활발하게 다뤄지는 분야들
    - 고객 서비스
    - 가격 예측
    - 포트폴리오 관리
    - 사기 탐지
    - 트레이딩 자동화
    - 리스크 관리
    - 신용 평가
    - ...

이번 글에서는 '특정' 주제를 다룰 것이다!라고 단정짓지 않고 금융에서 나타나고 있는 전반적인 변화에서, 특히 딥러닝 기술이 어떻게 활용될 수 있는지 서베이(Survey)해보기 위해 작성, 정리한 글입니다. 또, 딥러닝에 관점을 맞추는 이유는 더욱더 불확실해지고 있는 데이터를 다루기 위한 해결 요소로서 딥러닝 기술이 최적화되어 있다고 판단하기 때문입니다. 뿐만 아니라 금융업계는 지금까지 엄청난 고객 데이터를 쌓아왔기 때문에 이를 적극 활용하여 포스트 코로나 시대에서의 또다른 가치를 만들어내기 위해 힘쓰고 있습니다.
물론 아직까지는 알고리즘, 머신러닝을 활용한 기술을 무시할 수 없다는 점도 알려드립니다.

  • 이야기 순서
    • 금융업계의 숨겨진 힘: 고객 데이터
    • 고객 데이터에 딥러닝을?(금융업 관점)

금융업계의 숨겨진 힘: 고객 데이터

우리가 금융과 딥러닝을 함께 생각했을 때 가장 쉽게 떠오를 수 있는 것은 트레이딩(Trading) 관련 내용입니다. 고객은 돈만 주면 알아서 돈을 벌어오는 그런 구조요. 이에 관련해서는 대표적으로 크래프트 테크놀로지스라는 회사가 눈에 띕니다.

'적절한 타이밍에 사고/팔고 싶어! 근데 내가하긴 싫고, AI 너가 해. 그리고 돈 벌어와!'

딥러닝을 잘 모르는 고객이라면 매우 흥미로운 서비스입니다. 수시로 주가를 모니터링하지 않아도 AI가 알아서 적절한 상황을 판단하여 사고, 팔아 이익을 가져다주기 때문이죠. 여기서 잠깐, 고객의 입장과 트레이딩에 관한 기술도 매우 흥미롭지만, 우리는 전사적 차원으로 접근할 것입니다. 인공지능 기술을 탑재한 트레이딩처럼 엄청난 서비스를 고객에게 제공함으로써 자사 플랫폼에 머물게 하고 더 많은 기능을 사용하게끔 유도하게 됩니다(최근 금융업계는 플랫폼으로의 전환을 꾀하고 있다는 점을 무시할 수 없습니다). 또, 이를 달성하기 위해 먹이를 찾아나서는 기존의 행동 방식과 다르게 기술과 데이터를 적극적으로 활용하려는 움직임을 보이고 있죠.

  • 카카오의 카카오페이와 뱅크
  • 네이버의 네이버페이
  • 신한, KB, 우리 등 국내 금융업계

주식 예측, 펀딩 등을 수행하는 이런 기술은 매력적이지만 실제로 수많은 불가항적인 요소에 의해 적용, 구현하기 매우 어렵습니다. 향후 몇년 이후에는 진짜 상용화가 가능한, B2C가 가능한 Trading-Bot이 나올지도 모르겠지만, 지금은 아닐거에요.

주식 거래는 우리에게 익숙한 주제이고, 많은 사람이 이를 통해 큰 수익을 얻고자 하기 때문에 이를 통해 잠시 서론을 꾸며보았습니다. 사실 금융업계가 가지고 있는 진짜 힘은 이런 기술적인 부분이 아니라 수년간 쌓아오고, 정제해온 고객 데이터에 있습니다. 고객의 성별, 나이, 주소는 물론 거래 내역, 대출, 신용 정도 등 엄청나게 다양한 매력적인 특성(Feature)을 가지고 있습니다. 그리고 이들을 활용해서 마치 넷플릭스처럼, 유튜브처럼 뛰어난 추천 시스템을 개발해서 맞춤형 서비스를 제공하거나 미래의 행동을 미리 예측하여 그에 알맞는 고객 관리 방법을 적용할 수 있게끔 많은 노력을 기울이고 있습니다.

  • 고객 데이터로 다뤄볼 수 있는 것들
    • 추천을 통한 맞춤형 서비스 제공
    • AI 상담원 - 고객 서비스
    • 자산/리스크 관리
    • 이탈 고객 예측
    • 이 외에도 엄청 많아요.

쌓아둔 데이터도 많고, 딥러닝도 많이 발전했으니 이제 모델링을 통해 엄청 훌륭한 모델을 만들고,
고객에게 빨리 제공해서 이익을 창출할 수 있지 않을까요?

매우 힘들고 많은 시간이 소요될겁니다. 핵심적인 이유는 금융업계가 보유하고 있는 대부분의 고객 데이터가 정제되지 않은 상태라는 점 때문입니다. 또, 적재된 데이터의 특성과 양이 많아 이를 정제하는데 엄청난 비용이 소모되는 것도 사실입니다. 아마 우리가 데이터 분석을 배울때, 전체 시간의 80%는 데이터 파악에 활용한다!라고 배우는 것처럼 모델링에 투자하는 시간보다 기존 데이터를 정제하고 다시 쌓는 시간이 매우 오래 걸릴 것입니다. 하물며, 모델링을 수행한다고 해서 높은 성능을 기대하기란 매우 어려울거에요(빠른, 많은 A/B Test가 더욱 중요해지는 시점).

고객 데이터는 설계에 따라 지저분해진다면 제한없이 지저분해질 수 있는 장점(?)을 보유하고 있습니다. 이 때문에 고객 데이터를 다룸에 있어서 유명한 말인 'Garbage In, Garbage Out'이라는 말이 더욱 와닿는 것 같습니다. 부가설명이 필요없겠죠?

그렇다면 공격적인 투자도 좋지만, 선례를 조사해서 우리에게 알맞는 기술 적용을 검토해보자라는 느낌으로 금융업계에서 고객 데이터와 딥러닝을 활용해 어떤 서비스를 제공하고 있는지 더 살펴보죠.

 

고객 데이터에 딥러닝을?(금융업 관점)

1. 상품 추천과 마케팅

사실 이 부분은 따로 다루기 보다 유튜브나 넷플릭스의 추천 시스템 관점에서 생각하는 게 제일 적합하다고 생각합니다. 그럼에도 불구하고 조금만 코멘트를 추가해보죠.

우리가 자주 접하는 쇼핑몰에서의 의류, 전자 제품 등만 상품을 떠오를 수 있지만, 금융업에서 제공할 수 있는 상품은 이를테면, 예/적금 상품, 대출 상품, 보험 상품 등이 있습니다. 상품 추천이 중요한 이유는 고객을 끌어들이는 것도 있지만, 금융업계에서는 이탈 고객을 방지하는 것도 그에 못지않게 엄청 중요하기 때문입니다.

보통의 금융업계는 이탈 고객을 방지하기 위해 약간의 머신러닝 방법을 사용하는 경우나 그렇지 않은 경우를 포함해서 메일 또는 문자 메시지를 발송합니다. 일반적으로 '메일이나 문자를 주기적으로 보내서 규모의 경제처럼 고객 이탈을 방지하면 되지 않을까?'라고 생각할 수 있지만, 기업 입장에서는 이게 전부 '돈'입니다. 마케팅 비용이죠.

고객의 향후 패턴이나 특성을 고려하지 않은 내용을 포함한 메일/문자를 5번 보내서 이탈 고객 1명을 방지할 것을 딥러닝 모델을 사용해서 고객을 더욱 정확히 파악하고 이를 통해 1번의 문자로 고객의 이탈을 방지할 수 있습니다. 이를 위해서는 고객이 무엇을 원하는지를 정확히 추천할 필요가 있기 때문에 추천 시스템의 중요성은 더욱 높아지고 있습니다.

 

2. 이상 거래 탐지

이상 거래 탐지(Fraud Detection System)는 금융업계에서 아주 활발히 연구되고 있는 분야입니다. 신한, 핀크, 농협 등 FDS의 고도화를 진행하고 있습니다.

기존 룰 기반(Rule-base)으로 이상 거래를 탐지하던 방식은 시간이 지날수록 정교화되는 거래 사기를 잡아내기엔 제한적이기 때문에 많은 금융업계에서 머신러닝, 딥러닝 방법 적용에 큰 관심을 두고 개발하여 적용하고 있습니다. 여기서 사용될 모델은 기존 고객이 사용하던 거래 패턴과 사기 거래 패턴의 전체적인 맥락을 파악해서 차이점을 인식한 후 사기인지 아닌지를 우리에게 알려주어야 합니다. 그렇기에 불규칙적인 패턴을 잘잡아낼 수 있다는 큰 장점을 가진 딥러닝 모델이 관심을 받는 것이기도 합니다.
(물론 이를 위해 사람의 엄청난 노가다가..).

대표적으로 RF(Random Forest), SVM(Support Vector Machine), Machine&Deep Learning 방법을 활용해볼 수 있습니다. 이 방법들은 장점도 있는 반면, 정상/비정상 거래 데이터의 불균형, 데이터 전처리에서 사용되는 차원 축소의 효율성 문제, 실시간 탐지 특성을 유지하기 위한 컴퓨팅 파워 문제 등이 존재합니다.

대표적으로 캐글의 IEEE-CIS Fraud Detection 대회를 통해 이를 체험해 볼 수 있으며, Auto-Encoder나 LGBM과 같은 부스팅 계열 모델이 상위권을 차지하고 있는 것을 알 수 있습니다.

 

3. 이탈 고객 예측

1번에서 상품 추천 시스템을 매우 잘만들어 고객이 '이탈'이라는 생각을 하지 못하게 만들수도 있지만, 고객은 언제 변심할지 모릅니다.

이를 미연에 방지하고자 이탈 고객을 미리 예측하고, 이들에게 더욱 맞춤화된 서비스를 추천하고자하는 측면에서 기술을 적용할 수 있습니다.
- 기존 고객에게 A 제품을 마케팅할 때, 예측 모델을 통해 찾아낸 이탈 고객에게는 A 제품과 고객 특성을 파악해 얻어낸 B 제품을 동시에 추천!

고객 이탈을 예측하기 위해서는 고객의 행동적/통계적 특성을 동시에 활용해야 합니다. 물론 활용을 위해선 적재된 다양한 고객 데이터가 있어야 하는데, 이러한 점에서 금융업계는 금상첨화입니다.

기존의 Regression, Naive Bayes, Clustering, Boosting 모델과 자주 사용된 Random Forest 방법은 물론, 특성에 따라 CNN, RNN, LSTM, Auto-Encoder에서 GNN(Graph Neural Network)까지를 고려해볼 수 있습니다. 또한, Attention 모델을 사용해서 고객이 왜 이탈했는지 해석하려고 노력한 논문도 보였네요. 

 

4. 고객 서비스

고객이 서비스를 사용하다가 (간단한) 문제에 부딪혔습니다. 해결 방법을 물어보기 위해 상담원과 통화를 원하지만 상담 고객이 밀려 있어 5분, 10분, 15분, ... 하염없이 기다리다보면 회사의 서비스에 대한 고객의 불평은 커지기 마련입니다. 상담원, 마케터는 더 많은 고객의 궁금증을 내일로 미루지 않기를 원하지만, 반복적인 단순 질문으로 인해 처리가 지연될 뿐만 아니라 일의 효율성이 매우 떨어진다는 것을 자주 느낍니다.

고객 서비스를 위해 모든 금융업계가 챗봇 시스템 도입을 수행하고 있는 것은 이제금 자명한 사실이 되었습니다. 챗봇 시스템은 위의 문제를 해결해줄 수 있는 기술이자 고객에게 최고의 서비스를 제공할 수 있습니다. 챗봇은 유행성 기술이기도 하기 때문에 고객들이 필요한 정보와 얼마나 알맞는 정보를 제공할 수 있는지는 의문이라는 걱정어린 말도 엿들을 수 있습니다. 이에 대한 시나리오나 설계는 아직 모두 사람이 하고 있기 때문입니다.

아직 완벽하진 않지만, 이를 해결하기 위해 챗봇에도 추천, 랭킹 기술을 도입하는 방법이 상당 부분 고려되고 있습니다. 상담원에게 고객 대응을 위한 문장을 추천한 후, 상담원은 고객이 원하는 답변을 알맞게 변형하여 빠르게 답변할 수 있습니다.

고객 서비스에 정확한 대응을 하기 위해서는 통계적 특성과 상담 내용(음성 또는 텍스트) 등 다양한 데이터를 모델링에 사용할 수 있습니다. 또, 상담 내용 데이터는 챗봇 성능 고도화에 사용될 뿐만 아니라 추가적으로 분석해서 고객 성향과 향후 추천해주면 좋을 법한 제품까지 예측하도록 도와줄 수 있는 고급 데이터입니다.

딥러닝을 활용하여 다음과 같은 서비스를 제공할 수 있습니다.

  • 자연어 처리/생성에 사용되는 모델을 활용하여 고객의 특정 니즈를 파악하고, 감정을 분석하여 그에 알맞는 대응과 서비스를 제공할 수 있음
  • 24시간 항시 이용할 수 있는 AI 서비스

이 또한, 계속 생성되는 데이터를 실시간으로 학습하여 고객에게 더움 맞춤화된 해결 방법을 제공할 수 있기 때문에 고객 데이터의 확보가 매우 중요한 분야입니다.


사실 서비스 측면에서 파악하여 글을 작성하기로 한 것은 아니고, 세부적인 기술을 보고 싶었는데...

금융업 특성상 기술 공개가 그렇게 활발하지 않은 점과 동시에 많은 연구 논문들이 거의 비슷한 방법으로 method를 제안하고 있어 전혀 다른 결과의 글이 나온 점이 아쉽지만, 나름대로의 생각을 정리한 글입니다.

이번 글을 통해 어느 정도의 카테고리를 파악했으니, 다음 글은 데이터 처리나 모델링을 세부적으로 다뤄볼 수 있도록 노력을....

Youtube, NexFlix, Amazon 등 다양한 글로벌 및 국내 기업에서 추천 시스템은 매출 향상에 핵심적인 시스템이라고 표현해도 과언은 아닙니다. 또, 카카오, 네이버 등 음악이나 상품 서비스를 제공하고 있는 기업은 지금은 당연스럽게 사용하고 있는 기술이기 때문에 필수적으로 알아두면 좋습니다.

그뿐만 아니라 추천 시스템을 구성함에 있어서 최근 동향은 전통적인 통계 방법도 사용되지만 딥러닝 모델을 활용한 추천 시스템이 굉장히 많이 사용되고 있고, 전환되고 있는 것 같습니다.

이번 글에서도 간략하게 언급하겠지만, 모델 구조라던가 상세한 내용에 관해서는 아래 리뷰 논문을 자세히 살펴보는 것을 강력히 추천드리고, reference를 쫓아쫓아 공부하다 보면 2019년까지의 추천시스템 동향은 전부...

Are We Really Making Much Progress? A Worrying Analysis of Recent Neural Recommendation Approaches, RecSys 19


기본적으로 기존에 사용되던 추천 시스템은 협업 필터링과 콘텐츠 기반 필터링으로 나눌 수 있습니다.

 >> 협업 필터링(Collaborative Filtering, CF)

협업 필터링은 집단지성을 적극 활용한 기술입니다. 즉, 사용자의 행태를 분석하고 이와 유사한 행태를 가진 사용자에게 같은 제품을 추천하는 방식입니다. 이를 사용자 기반 협업 필터링(User-based Collaborative Filtering)라고 할 수 있습니다.
추가로 여기서 말하는 필터링(FIltering)이란, 여러 가지 항목 중 적당한 항목을 선택하는 것을 의미하는 용어입니다.

대표적으로 행렬 분해(Matrix Factorization), k-NN 등 방법이 사용됩니다.

하지만 협업 필터링에는 크게 세 가지 단점이 있습니다.

  1. 콜드 스타트(Cold Start)
    - 기존 데이터에 완전히 포함되지 않는 새로운 유형의 데이터를 만났을 때입니다. 당연히 협업 필터링은 기존 데이터를 기반으로 추천을 수행하기 때문에 큰 문제에 해당합니다.
  2. 계산 효율성
    - 기존 딥러닝에서 성능과 속도의 Trade-off 관계를 떠올리면 쉽게 이해할 수 있습니다. 기존 데이터를 많이 활용할 수록 정확한 추천을 수행할 수 있지만, 계산 측면에서 비효율적입니다.
  3. 극소수 데이터를 무시
    - 정규 분포를 떠올리면 쉽습니다. 사용자들이 유명한 제품을 주로 구매하는 것은 자명한 사실이죠. 기존 데이터가 쏠려있음으로서 소수 사용자들에게 선택받은 제품이 다른 사용자에게 추천되지 못하는 상황을 만듭니다.

 >> 콘텐츠 기반 협업 필터링(Content-based Collaborative Filtering)

콘텐츠 기반 협업 필터링은 아이템 기반 협업 필터링(Item-based Collaborative Filtering)이라고 표현하기도 하며, 협업 필터링처럼 사용자가 제품을 구매하면서 남기는 행태나 기록을 데이터로 사용하지 않고, 사용자가 선택한 항목을 분석하여 이와 유사한 항목을 추천해주는 방법입니다.

  • 음악을 분석하는 경우에는 장르, 비트, 음색 등 다양한 특징을 추출하여 이를 분석하고 유사한 형태의 곡을 사용자에게 추천해주는 것이죠.

대표적으로 딥러닝을 활용한 방법, 자연어 처리에 사용되는 latent vector 방법, tf-idf 등이 사용됩니다.

콘텐츠 기반 필터링 방법은 항목 자체를 분석하기 때문에 신규 고객에게도 유사한 항목을 추천해줄 수 있어 콜드 스타트를 해결할 수 있다는 장점이 존재합니다. 하지만 데이터 특성을 구성하는 과정이 조금 어렵다는 단점도 존재합니다.

  • 예를 들어, table data와 image data 특성을 어떤 형태로 concat해서 모델에 입력시킬 것인지 등.

위에서 살펴본 기존 두 가지 방법은 현재도 (변형해서)사용되고 있는 꽤나 훌륭한 방법입니다. 하지만 단점도 존재하기에 장점만 혼합한 방법을 사용하면 더 좋지 않을까 싶습니다.

 >> 하이브리드 협업 필터링(Hybrid Collaborative Filtering)

하이브리드 협업 필터링은 간단하게 협업 필터링과 콘텐츠 기반 필터링을 함께 사용하는 겁니다. 새로운 데이터가 들어왔을때 콜드 스타트 문제를 해결하기 위해 콘텐츠 기반 협업 필터링을 사용하다가 새로운 유형의 데이터가 시간이 지남에 따라 쌓인 후에 사용자 기반 협업 필터링으로 전환하는 방법입니다.

 

Reference

www.kocca.kr/insight/vol05/vol05_04.pdf

 

예전에 Pytorch의 Image augmentation 방법을 보고 커스텀이 되게 편리하게 구성된 것 같아 TensorFlow도 이와 같은 방법으로 augmentation하도록 만들어줬으면 좋겠다는 생각을 하고 있었는데, 연동 가능한 라이브러리 albumentation이 있었습니다.
(만들어진지 좀 됬는데 늦게 알게 됨..)

transforms.Compose([
	transforms.CenterCrop(10),
	transforms.ToTensor(),
])

torchvision의 augmentation 방법

사실 아래 공식 홈페이지 튜토리얼 코드를 보면 TensorFlow도 이와 같은 방법으로 Keras Layer를 활용한 augmentation이 가능토록 제공하고 있고, 앞으로도 이러한 방법으로 제공할 예정인가 싶습니다.

data_augmentation = tf.keras.Sequential([
  layers.experimental.preprocessing.RandomFlip("horizontal_and_vertical"),
  layers.experimental.preprocessing.RandomRotation(0.2),
])

출처: www.tensorflow.org/tutorials/images/data_augmentation?hl=ko


tf.image를 사용하는 방법

albumentation 라이브러리를 사용해보기 전에, tf.image를 사용해서 어떻게 augmentation 할 수 있는지 코드를 첨부합니다.

test = tf.data.Dataset.from_tensor_slices(dict(df))

# 이미지와 레이블을 얻습니다.
def get_image_label(dt):
    img_path = dt['image']
    
    image = tf.io.read_file(img_path)
    image = tf.image.decode_jpeg(image)
    image = tf.image.convert_image_dtype(image, tf.float32)
    image = tf.image.resize(image, [IMG_SIZE, IMG_SIZE])
    image = (image / 255.0)

    label = []
    
    for key in class_col:
        label.append(dt[key])
    
    return image, label

# data augment
# refer: https://www.tensorflow.org/tutorials/images/data_augmentation
def augment(image,label):
    # Add 6 pixels of padding
    image = tf.image.resize_with_crop_or_pad(image, IMG_SIZE + 6, IMG_SIZE + 6) 
    # Random crop back to the original size
    image = tf.image.random_crop(image, size=[IMG_SIZE, IMG_SIZE, 3])
    image = tf.image.random_brightness(image, max_delta=0.5) # Random brightness
    image = tf.clip_by_value(image, 0, 1)

    return image, label

    
dataset = test.map(get_image_label)
dataset = dataset.shuffle(50).map(augment).batch(4)

이 방법도 괜찮긴하지만 문제는 tf.Dataset 작동 구조를 알아야 정확히 쓸 수 있습니다.
map 함수 작동 방식이라던가, shuffle과 batch의 위치 등등..

확실히 위 방법보다 augmentation 방법을 Layer 구조로 가져가는게 훨씬 가독성이 좋아보이기도 합니다. 아래처럼요.

model = tf.keras.Sequential([
  resize_and_rescale,
  data_augmentation,
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  # Rest of your model
])

resize_and_rescale과 data_augmentation을 Sequential 안에 Layer 형태로 사용하고 있습니다.

하지만 아직 많이 쓰이고 있는 방법은 아닌 것 같기에 확실하게 자리잡기 전(?)까지 다른 방법을 사용해도 꽤나 무방합니다.

 

Albumentation 라이브러리를 사용하는 방법

사용하는 방법은 꽤나 단순합니다. 위에서 예제로 보여드렸던 코드와 형식이 동일하니까요.

먼저, 필요 라이브러리를 불러옵니다.

from tensorflow.keras.datasets import cifar10

import albumentations
import cv2
import numpy as np
import matplotlib.pyplot as plt
import cv2
# augmentation method를 import합니다.
from albumentations import (
    Compose, HorizontalFlip, CLAHE, HueSaturationValue,
    RandomBrightness, RandomContrast, RandomGamma,
    ToFloat, ShiftScaleRotate
)

torchvision의 Compose, TensorFlow의 Sequential과 Albumentation의 Compose의 사용되는 장소가 같습니다. 위에서 호출한 함수 외에도 다양한 augmentation 방법을 제공합니다. 더 궁금하면 공식 문서를 참조하고, 자세히 설명되어 있습니다.

albumentations.ai/docs/getting_started/image_augmentation/

이제 다양한 함수를 Compose안에 list 형태로 제공하고, 사용할 준비를 끝마칩니다.

# 각 함수에 대한 설명은
# https://albumentations.ai/docs/
# document를 참고하세요.
Aug_train = Compose([
    HorizontalFlip(p=0.5),
    RandomContrast(limit=0.2, p=0.5),
    RandomGamma(gamma_limit=(80, 120), p=0.5),
    RandomBrightness(limit=0.2, p=0.5),
    HueSaturationValue(hue_shift_limit=5, sat_shift_limit=20,
                       val_shift_limit=10, p=.9),
    ShiftScaleRotate(
        shift_limit=0.0625, scale_limit=0.1, 
        rotate_limit=15, border_mode=cv2.BORDER_REFLECT_101, p=0.8), 
    ToFloat(max_value=255)
])

Aug_test = Compose([
    ToFloat(max_value=255)
])

 

실험하기에 가장 좋은 예제는 tensorflow.keras.datasets에서 제공하는 CIFAR-10입니다.
또, Sequence 클래스를 상속받아 제네레이터처럼 활용할 수 있도록 합니다.
아래 코드 __getitem__ 부분의 self.augment(image=x)["image"]에서 변환 작업이 수행됩니다.

from tensorflow.python.keras.utils.data_utils import Sequence

# Sequence 클래스를 상속받아 generator 형태로 사용합니다.
class CIFAR10Dataset(Sequence):
    def __init__(self, x_set, y_set, batch_size, augmentations):
        self.x, self.y = x_set, y_set
        self.batch_size = batch_size
        self.augment = augmentations

    def __len__(self):
        return int(np.ceil(len(self.x) / float(self.batch_size)))

    # 지정 배치 크기만큼 데이터를 로드합니다.
    def __getitem__(self, idx):
        batch_x = self.x[idx * self.batch_size:(idx + 1) * self.batch_size]
        batch_y = self.y[idx * self.batch_size:(idx + 1) * self.batch_size]
        
        # augmentation을 적용해서 numpy array에 stack합니다.
        return np.stack([
            self.augment(image=x)["image"] for x in batch_x
        ], axis=0), np.array(batch_y)

# CIFAR-10 Dataset을 불러옵니다.
(x_train, y_train), (x_test, y_test) = cifar10.load_data()

BATCH_SIZE = 8

# Dataset을 생성합니다.
train_gen = CIFAR10Dataset(x_train, y_train, BATCH_SIZE, Aug_train)
test_gen = CIFAR10Dataset(x_test, y_test, BATCH_SIZE, Aug_test)

train_gen을 통해 이미지를 그려보면 다음과 같이 변환이 일어난 것을 볼 수 있습니다.

# 데이터를 그려봅시다.
images, labels = next(iter(train_gen))

fig = plt.figure()

for i, (image, label) in enumerate(zip(images, labels)):
    ax = fig.add_subplot(3, 3, i + 1)
    ax.imshow(image)
    ax.set_xlabel(label)
    ax.set_xticks([]); ax.set_yticks([])

plt.tight_layout()
plt.show()

Augmentation이 수행된 이미지

 

Reference

텐서플로우 공식 홈페이지

Albumentation Document

medium.com/the-artificial-impostor/custom-image-augmentation-with-keras-70595b01aeac

이 글은 keras 공식 문서의 'Few-Shot learning with Reptile' 글을 번역한 것입니다.


Introduction

Reptile 알고리즘은 Meta-Learning을 위해 open-AI가 개발한 알고리즘입니다. 특히, 이 알고리즘은 최소한의 training으로 새로운 task를 수행하기 위한 학습을 빠르게 진행하기 위해 고안되었습니다(Few-Shot learning). Few-shot learning의 목적은 소수의 데이터만 학습한 모델이 보지 못한 새로운 데이터를 접했을 때, 이를 분류하게 하는 것입니다. 예를 들어, 희귀병 진단이나 레이블 cost가 높은 경우에 유용한 방법이라고 할 수 있습니다. 즉, 해결해야 될 문제에서 데이터가 적을때 충분히 고려해볼만한 방법입니다.

알고리즘은 이전에 보지 못한 데이터의 mini-batch로 학습된 가중치와 고정된 meta-iteration을 통해 사전 학습된 모델 가중치 사이의 차이를 최적화하는 SGD와 함께 작동합니다. 즉, n번 만큼의 횟수에서 mini-batch로 학습하여 얻은 가중치와 모델 weight initialization 차이를 최소하하면서 optimal weight를 update하게 됩니다.
(동일 클래스 이미지에 계속해서 다른 레이블로 학습하는데 어떻게 정답을 맞추는지 정말 신기합니다. 이 부분을 알려면 더 깊게 공부해봐야 알듯...)

필요 라이브러리를 임포트합니다.

import matplotlib.pyplot as plt
import numpy as np
import random
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import tensorflow_datasets as tfds

하이퍼파라미터 정의

learning_rate = 0.003 # 학습률
meta_step_size = 0.25 # 메타 스템 크기

inner_batch_size = 25 # 학습 배치 크기
eval_batch_size = 25 # 테스트 배치 크기

meta_iters = 2000 # 학습 총 횟수
eval_iters = 5 # 평가 데이터셋 반복 횟수
inner_iters = 4 # 학습 데이터셋 반복 횟수

eval_interval = 1 # 평가 기간 설정
train_shots = 20 # 학습에서 각 클래스마다 몇 개 샘플을 가져올지
shots = 5 # 평가에서 각 클래스마다 몇 개 샘플을 가져올지
classes = 5 # 몇 개 클래스를 사용할지

여기서는 5-way 5-shot이라고 표현할 수 있겠네요(N-way, k-shot)
--> 5개 클래스에서 5장 뽑아서 이를 학습에 사용하겠다는 의미

 

데이터 준비

Omniglot 데이터셋은 각 character별로 20개 샘플을 가지고 있는 50가지 알파벳(즉, 50개의 다른 언어)으로부터 얻어진 1,623개 데이터로 이루어져 있습니다(서로 다른 나라의 문자에서 특정 클래스들을 모아둔 데이터셋입니다). 여기서 20개 샘플은 Amazon's Mechanical Turk으로 그려졌습니다.

few-shot learning task를 수행하기 위해 k개 샘플은 무작위로 선택된 n개 클래스로부터 그려진 것을 사용합니다. n개 숫자값은 몇 가지 예에서 새 작업을 학습하는 모델 능력을 테스트하는 데 사용할 임시 레이블 집합을 만드는 데 사용됩니다. 예를 들어, 5개 클래스를 사용한다고 하면, 새로운 클래스 레이블은 0, 1, 2, 3, 4가 될 것입니다.

핵심은 모델이 계속 반복해서 class instance를 보게 되지만, 해당 instance에 대해서는 계속해서 서로 다른 레이블을 보게 됩니다. 이러한 과정에서 모델은 단순하게 클래스를 분류하는 방법이 아니라 해당 데이터를 특정 클래스로 구별하는 방법을 배워야 합니다.

Omniglot 데이터셋은 다양한 클래스에서 만족할만한 개수의 샘플을 가져다 사용할 수 있기 때문에 이번 task에 적합합니다.

class Dataset:
    # 이 클래스는 few-shot dataset을 만듭니다.
    # 새로운 레이블을 만듬과 동시에 Omniglot 데이터셋에서 샘플을 뽑습니다.
    def __init__(self, training):
        # omniglot data를 포함한 tfrecord file을 다운로드하고 dataset으로 변환합니다.
        split = "train" if training else "test"
        ds = tfds.load("omniglot", split=split, as_supervised=True, shuffle_files=False)
        # dataset을 순환하면서 이미지와 클래스를 data dictionary에 담을겁니다.
        self.data = {}

        def extraction(image, label):
            # RGB에서 grayscale로 변환하고, resize를 수행합니다.
            image = tf.image.convert_image_dtype(image, tf.float32)
            image = tf.image.rgb_to_grayscale(image)
            image = tf.image.resize(image, [28, 28])
            return image, label

        for image, label in ds.map(extraction):
            image = image.numpy()
            label = str(label.numpy())
            # 레이블이 존재하지 않으면 data dictionary에 넣고,
            if label not in self.data:
                self.data[label] = []
            # 존재하면 해당 레이블에 붙여줍니다.
            self.data[label].append(image)
            # 레이블을 저장합니다.
            self.labels = list(self.data.keys())

    def get_mini_dataset(self, batch_size, repetitions, shots, num_classes, split=False):
        # num_classes * shots 수 만큼 이미지와 레이블을 가져올 것입니다.
        temp_labels = np.zeros(shape=(num_classes * shots))
        temp_images = np.zeros(shape=(num_classes * shots, 28, 28, 1))
        if split:
            test_labels = np.zeros(shape=(num_classes))
            test_images = np.zeros(shape=(num_classes, 28, 28, 1))
          
        # 전체 레이블 셋에서 랜덤하게 몇 개의 레이블만 가져옵니다.
        # self.labels는 omniglot에서 가져온 데이터의 전체 레이블을 담고 있습니다.
        # 전체 레이블에서 num_classes만큼 label을 랜덤하게 뽑습니다.
        label_subset = random.choices(self.labels, k=num_classes)
        for class_idx, _ in enumerate(label_subset):
            # few-shot learning mini-batch를 위한 현재 레이블값으로 enumerate index를 사용합니다.
            # temp_label에서는 shots 수만큼 class_idx를 가지게 됩니다.[0, 0, 0, 0, 0]
            # 해당되는 이미지의 레이블은 계속해서 변하는 점을 참고
            temp_labels[class_idx * shots : (class_idx + 1) * shots] = class_idx

            # 테스트를 위한 데이터셋을 만든다면, 선택된 sample과 label을 넣어줍니다.
            # 테스트 과정에서 temp_label과 test_label은 동일해야 합니다.
            if split:
                test_labels[class_idx] = class_idx
                images_to_split = random.choices(self.data[label_subset[class_idx]], 
                                                 k=shots + 1)
                test_images[class_idx] = images_to_split[-1]
                temp_images[class_idx * shots : (class_idx + 1) * shots] = images_to_split[:-1]
            else:
                # label_subset[class_idx]에 해당하는 이미지를 shots만큼 temp_images에 담습니다.
                # 이로써 temp_images는 label_subset[class_idx] 레이블에 관한 이미지를 가지고 있지만,
                # temp_labels는 이에 상관없이 class_idx 클래스를 가집니다.
                temp_images[
                    class_idx * shots : (class_idx + 1) * shots
                ] = random.choices(self.data[label_subset[class_idx]], k=shots)

        # TensorFlow Dataset을 만듭니다.
        dataset = tf.data.Dataset.from_tensor_slices(
            (temp_images.astype(np.float32), temp_labels.astype(np.int32))
        )
        dataset = dataset.shuffle(100).batch(batch_size).repeat(repetitions)

        if split:
            return dataset, test_images, test_labels
            
        return dataset


import urllib3

urllib3.disable_warnings()  # Disable SSL warnings that may happen during download.
train_dataset = Dataset(training=True)
test_dataset = Dataset(training=False)

 

모델 구성

def conv_bn(x):
    x = layers.Conv2D(filters=64, kernel_size=3, strides=2, padding="same")(x)
    x = layers.BatchNormalization()(x)
    return layers.ReLU()(x)


inputs = layers.Input(shape=(28, 28, 1))
x = conv_bn(inputs)
x = conv_bn(x)
x = conv_bn(x)
x = conv_bn(x)
x = layers.Flatten()(x)
outputs = layers.Dense(classes, activation="softmax")(x)

model = keras.Model(inputs=inputs, outputs=outputs)
model.compile()
optimizer = keras.optimizers.SGD(learning_rate=learning_rate)

 

모델 학습

training = []
testing = []
for meta_iter in range(meta_iters):
    frac_done = meta_iter / meta_iters
    cur_meta_step_size = (1 - frac_done) * meta_step_size
    # 현재 모델 weights를 저장합니다.
    old_vars = model.get_weights()
    # 전체 데이터셋에서 샘플을 가져옵니다.
    mini_dataset = train_dataset.get_mini_dataset(
        inner_batch_size, inner_iters, train_shots, classes
    )

    for images, labels in mini_dataset:
        with tf.GradientTape() as tape:
            preds = model(images)
            loss = keras.losses.sparse_categorical_crossentropy(labels, preds)

        grads = tape.gradient(loss, model.trainable_weights)
        optimizer.apply_gradients(zip(grads, model.trainable_weights))

    # 위의 loss로 업데이트된 weight를 가져옵니다.
    new_vars = model.get_weights()
    # meta step with SGD
    for var in range(len(new_vars)):
        new_vars[var] = old_vars[var] + (
            (new_vars[var] - old_vars[var]) * cur_meta_step_size
        )

    # meta-learning 단계를 수행했다면, 모델 가중치를 다시 업데이트해줍니다.
    model.set_weights(new_vars)

    # Evaluation loop
    if meta_iter % eval_interval == 0:
        accuracies = []
        for dataset in (train_dataset, test_dataset):
            # 전체 데이터셋에서 mini dataset을 가져옵니다.
            train_set, test_images, test_labels = dataset.get_mini_dataset(
                eval_batch_size, eval_iters, shots, classes, split=True
            )
            old_vars = model.get_weights()
            # Train on the samples and get the resulting accuracies.
            for images, labels in train_set:
                with tf.GradientTape() as tape:
                    preds = model(images)
                    loss = keras.losses.sparse_categorical_crossentropy(labels, preds)

                grads = tape.gradient(loss, model.trainable_weights)
                optimizer.apply_gradients(zip(grads, model.trainable_weights))
            
            test_preds = model.predict(test_images)
            test_preds = tf.argmax(test_preds).numpy()
            num_correct = (test_preds == test_labels).sum()

            # 평가 acc를 얻었다면 다시 weight를 초기화해야합니다.
            model.set_weights(old_vars)
            accuracies.append(num_correct / classes)

        training.append(accuracies[0])
        testing.append(accuracies[1])

        if meta_iter % 100 == 0:
            print(
                "batch %d: train=%f test=%f" % (meta_iter, accuracies[0], accuracies[1])
            )

 

결과 시각화

# First, some preprocessing to smooth the training and testing arrays for display.
window_length = 100
train_s = np.r_[
    training[window_length - 1 : 0 : -1], training, training[-1:-window_length:-1]
]
test_s = np.r_[
    testing[window_length - 1 : 0 : -1], testing, testing[-1:-window_length:-1]
]
w = np.hamming(window_length)
train_y = np.convolve(w / w.sum(), train_s, mode="valid")
test_y = np.convolve(w / w.sum(), test_s, mode="valid")

# 학습 acc를 그립니다.
x = np.arange(0, len(test_y), 1)
plt.plot(x, test_y, x, train_y)
plt.legend(["test", "train"])
plt.grid()

train_set, test_images, test_labels = dataset.get_mini_dataset(
    eval_batch_size, eval_iters, shots, classes, split=True
)

for images, labels in train_set:
    with tf.GradientTape() as tape:
        preds = model(images)
        loss = keras.losses.sparse_categorical_crossentropy(labels, preds)

    grads = tape.gradient(loss, model.trainable_weights)
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

test_preds = model.predict(test_images)
test_preds = tf.argmax(test_preds).numpy()

_, axarr = plt.subplots(nrows=1, ncols=5, figsize=(20, 20))

for i, ax in zip(range(5), axarr):
    temp_image = np.stack((test_images[i, :, :, 0],) * 3, axis=2)
    temp_image *= 255
    temp_image = np.clip(temp_image, 0, 255).astype("uint8")
    ax.set_title(
        "Label : {}, Prediction : {}".format(int(test_labels[i]), test_preds[i])
    )
    ax.imshow(temp_image, cmap="gray")
    ax.xaxis.set_visible(False)
    ax.yaxis.set_visible(False)
plt.show()

 

Reference

Few-shot learning은 카카오 기술 블로그에도 설명되어 있습니다. >> 클릭

talkingaboutme.tistory.com/entry/DL-Meta-Learning-Learning-to-Learn-Fast

www.borealisai.com/en/blog/tutorial-2-few-shot-learning-and-meta-learning-i/

'# Machine Learning > TensorFlow doc 정리' 카테고리의 다른 글

tf.data tutorial 번역 (5)  (0) 2020.02.28
tf.data tutorial 번역 (4)  (1) 2020.02.27
tf.data tutorial 번역 (3)  (0) 2020.02.26
tf.data tutorial 번역 (2)  (0) 2020.02.18
tf.data tutorial 번역 (1)  (0) 2020.02.14