#인공지능 

MoveNet keypoints를 위한 TensorFlow custom dataloader 만들기

MoveNet을 이용해서 요가 이미지의 coarse label을 생성하는 방법과, 텐서플로우 custom dataloader로 요가 이미지와 keypoints 데이터 모두를 입력받는 모델을 구현하는 과정을 보여드립니다.

2022-11-09 | 박정현

모두의연구소 콘텐츠 크리에이터 모임 “코크리” 1기 활동으로 작성된 글입니다. (저자 블로그)


모델의 정확도가 향상될 수 있도록 MoveNet을 이용해 coarse label을 만들어 보는 튜토리얼입니다.

* 해당 포스트는 아래의 colab 실습 코드를 옮겨온 것입니다. colab의 실습 코드 및 콘텐츠를 읽으셔도 괜찮습니다. colab의 실행 결과는 캡처로 옮겼습니다.

Google Colaboratory

No Description

시작하며

콘텐츠를 만들게 된 계기

저는 model.fit() 벗어나기라는 콘텐츠를 만들고 있었습니다. 내용은 model.fit() 대신 Train loop를 만들어 직접 모델을 학습시킨다는 내용입니다. 총 두 편으로 구성되어있고 관심이 있으시다면 아래를 참고해주세요.

저는 제 콘텐츠에서 CNN 모델을 이용해 어떤 요가 포즈 classification을 학습하는 것을 보여주고 싶었습니다. 하지만 예제 코드를 만들면서 저는 제 콘텐츠 문제가 생겼다는 것을 깨달았습니다. 모범이 되어야 할 것만 같은 콘텐츠의 딥러닝 모델의 정확도가 너무 낮다는 것이었습니다. 그래서 저는 모델에게 이미지뿐만 아니라 더 많은 정보를 주기로 했습니다. 바로 딥러닝 모델에 Yogapose(요가를 하는 사람 이미지) 이미지와 해당 이미지에 있는 요가 중인 사람의 keypoints 정보까지 넣어주는 것이었습니다. 제가 사용하던 데이터를 다운받아보고 싶다면 이곳을 참고하세요.

제가 가지고 있었던 데이터는 왼쪽의 내 요가 데이터이고 annotation한 데이터처럼 keypoints들도 얻고 싶었습니다.

요가 데이터

[그림 1] 요가 데이터

위 [그림 1]의 annotation한 데이터는 keypoints들과 그 keypoints들을 각각 연결하는 선을 시각화해두었습니다. 이 keypoints의 표현 방법은 아래에서 계속 이야기하겠습니다!

직접 annotation 해야 하는 걸까

제가 원하는 것을 하기엔 전 이미지 데이터밖에 없었습니다. 즉, 제가 keypoints들을 직접 찍어줘야 한다는 것이었죠. 제가 직접 annotation을 해봤을 때 느낀 거 지만 목과 어깨가 너무도 뻐근하고 피곤합니다. 데이터 회사에 맡기기엔 비싸고, 직접 하기엔 너무 피곤하죠.. 직접 Annotation을 하지 않는 방법 중에서도 Self-Supervised Learning, Semi-Supervised Learning 모델들이 많이 나오고 있지만 여전히 딥러닝을 하기 위해서 data annotation에 대한 부담은 큽니다.

하기 싫어..

[그림 2] 하기 싫어..

저는 직접 하나하나 annotation 하기가 너무 싫었습니다! 그리고 저는 완벽하거나 정밀한 label이 아닌 coarse label을 만들어 일단 모델의 정확도가 향상되는지 보고 싶었습니다. 대충 coarse label을 만들어보기엔 이미 존재하는 keypoints detection 모델을 사용해 그 inference 값을 사용해보면 좋겠다는 생각이 문득 들었죠!

그래서 이미 잘 학습된 keypoints detection model을 찾아보았고, MoveNet: Ultra fast and accurate pose detection model를 알게 되었습니다. 한국어로 번역한다면 ‘짱 빠르고 정확한’이죠. 그 부분이 저를 사로잡았습니다. 저는 어느 정도 만들어진 label(여기서는 keypoints)로 대충 제 모델을 돌려보고 싶었거든요.

MoveNet

기쁘게도 TensorFlow는 아래에 있는 링크에서 제가 원하는 MoveNet을 colab에서도 돌릴 수 있도록 해놓았습니다. 오예 🔥🔥

MoveNet: Ultra fast and accurate pose detection model. | TensorFlow Hub

is an ultra fast and accurate model that detects 17 keypoints of a body. The model is offered on TF Hub with two variants, known as Lightning and Thunder. Lightning is intended for latency-critical applications, while Thunder is intended for applications that require high accuracy.

하지만 그냥 가져다 쓰면 뭔가 얍삽스럽기 때문에 위 링크에 있는 짤막한 MoveNet의 설명을 번역하고 약간의 설명을 덧붙이며 이 모델이 어떤 건지 알아가는 시간을 짧게 가져보려고 합니다.

MoveNet은 17개로 이루어진 바디 키포인트를 디텍팅하는 짱 빠르고 정확한 모델입니다. 이 모델은 TF Hub에서 Lightning과 Thunder로 알려진 두 가지 버전(variants)으로 제공됩니다. Lightning은 레이턴시(latency)가 중요한 어플리케이션을 위해 만들어졌고, Thunder는 높은 정확도를 요하는 어플리케이션을 위해 만들어졌습니다. 두 모델은 둘 다 실시간으로 (30+FPS) 데스크톱, 랩톱, 폰에서 사용할 수 있고, 피트니스, 헬스, 웰니스 어플리케이션으로 증명되었습니다. (부가 설명 : latency가 중요하다는 것은 속도가 중요하다는 것입니다.)

이 17개의 keypoints는 COCO 스탠다드를 따랐으며 그 keypoints의 종류는 다음과 같습니다!

“nose”, “left_eye”, “right_eye”, “left_ear”, “right_ear”, “left_shoulder”, “right_shoulder”, “left_elbow”, “right_elbow”, “left_wrist”, “right_wrist”, “left_hip”, “right_hip”, “left_knee”, “right_knee”, “left_ankle”, “right_ankle”

17개 keypoints

[그림 3] 17개 keypoints

우리가 아까 위에서 본 [그림 1] 오른쪽의 이미지를 생각해보세요! 총 17개의 keypoints(관절)와 그 관절사이를 잇는 선이 시각화되어 있습니다. [그림 3] 역시 마찬가지입니다. 그렇다면 제가 사용하려는 데이터는 정확히 무엇일까요? 맞습니다. 바로 이미지에 있는 사람의 17개 keypoints의 좌표 (x, y) 값입니다. 총 17개이고 각각 x, y 좌표를 가지고 있기 때문에 (17, 2) 사이즈인 것입니다.

자 이제 어떤 방식으로 제가 사용하고 있는 Yogapose 이미지에 annotation을 해보았는지 보여드릴 차례입니다. TensorFlow가 제공하는 코드와 제 머리에서 나온 코드의 콜라보를 보여드리겠습니다.

구글 드라이브 마운트하기

데이터를 매번 colab에 올렸다 내렸다 하는 건 불편합니다. colab은 세션이 끝나면 데이터가 싹 사라지기 때문입니다. 그래서 데이터를 구글 드라이브에 올려서 그곳에 둔 데이터를 가져와 사용합니다. 하지만 맨 처음에는 colab과 내 구글 드라이브는 마운트되어 있지 않기 때문에 마운트하는 과정이 필요합니다.

from google.colab import drive
drive.mount('/content/gdrive')

마운트를 하고 나면 아래 이미지와 같이 '/content/gdrive/MyDrive' 아래에 내 구글 드라이브에서 많이 본 디렉터리들이 줄지어 있을 것입니다.

마운트 성공!

[그림 4] 마운트 성공!

자 이제 데이터를 준비했으니 학습된 짱 빠르고 정확한 MoveNet을 가져와 봅시다.

MoveNet 모델 가져오기 & 환경설정하기

저는 movenet_lightning을 사용했습니다. 아까 말씀드렸던 것과 같이 lightning은 주로 빠른 속도를 원할 때 사용합니다. MoveNet으로부터 얻은 keypoints를 통해 학습할 제 CNN 모델의 정확도가 어느 정도 이상 향상되면 괜찮다고 생각했기 때문에 이 모델을 골랐습니다. 하지만 lightning보다는 느리지만 keypoints의 정확도가 조금 더 높은 것을 원하시는 분은 Thunder가 좋을 것입니다.

!pip install -q imageio
!pip install -q opencv-python
!pip install -q git+https://github.com/tensorflow/docs
import tensorflow as tf
import tensorflow_hub as hub
from tensorflow_docs.vis import embed
import numpy as np
import cv2

# Import matplotlib libraries
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
import matplotlib.patches as patches

# Some modules to display an animation using imageio.
import imageio
from IPython.display import HTML, display

model_name = "movenet_lightning"

if "tflite" in model_name:
    if "movenet_lightning_f16" in model_name:
        !wget -q -O model.tflite https://tfhub.dev/google/lite-model/movenet/singlepose/lightning/tflite/float16/4?lite-format=tflite
        input_size = 192
    elif "movenet_thunder_f16" in model_name:
        !wget -q -O model.tflite https://tfhub.dev/google/lite-model/movenet/singlepose/thunder/tflite/float16/4?lite-format=tflite
        input_size = 256
    elif "movenet_lightning_int8" in model_name:
        !wget -q -O model.tflite https://tfhub.dev/google/lite-model/movenet/singlepose/lightning/tflite/int8/4?lite-format=tflite
        input_size = 192
    elif "movenet_thunder_int8" in model_name:
        !wget -q -O model.tflite https://tfhub.dev/google/lite-model/movenet/singlepose/thunder/tflite/int8/4?lite-format=tflite
        input_size = 256
    else:
        raise ValueError("Unsupported model name: %s" % model_name)

    # Initialize the TFLite interpreter
    interpreter = tf.lite.Interpreter(model_path="model.tflite")
    interpreter.allocate_tensors()

    def movenet(input_image):
      """Runs detection on an input image.
      Args:
        input_image: A [1, height, width, 3] tensor represents the input image
          pixels. Note that the height/width should already be resized and match the
          expected input resolution of the model before passing into this function.
      Returns:
        A [1, 1, 17, 3] float numpy array representing the predicted keypoint
        coordinates and scores.
      """
      # TF Lite format expects tensor type of uint8.
      input_image = tf.cast(input_image, dtype=tf.uint8)
      input_details = interpreter.get_input_details()
      output_details = interpreter.get_output_details()
      interpreter.set_tensor(input_details[0]['index'], input_image.numpy())
      # Invoke inference.
      interpreter.invoke()
      # Get the model prediction.
      keypoints_with_scores = interpreter.get_tensor(output_details[0]['index'])
      return keypoints_with_scores

else:
  if "movenet_lightning" in model_name:
      module = hub.load("https://tfhub.dev/google/movenet/singlepose/lightning/4")
      input_size = 192
  elif "movenet_thunder" in model_name:
      module = hub.load("https://tfhub.dev/google/movenet/singlepose/thunder/4")
      input_size = 256
  else:
      raise ValueError("Unsupported model name: %s" % model_name)

  def movenet(input_image):
    """Runs detection on an input image.
    Args:
      input_image: A [1, height, width, 3] tensor represents the input image
        pixels. Note that the height/width should already be resized and match the
        expected input resolution of the model before passing into this function.
    Returns:
      A [1, 1, 17, 3] float numpy array representing the predicted keypoint
      coordinates and scores.
    """
    model = module.signatures['serving_default']

    # SavedModel format expects tensor type of int32.
    input_image = tf.cast(input_image, dtype=tf.int32)
    # Run model inference.
    outputs = model(input_image)
    # Output is a [1, 1, 17, 3] tensor.
    keypoints_with_scores = outputs['output_0'].numpy()
    return keypoints_with_scores

Annotation할 데이터 가져오기

Annotation할 즉, body keypoint가 필요한 이미지 데이터들을 가져옵니다.

import os
from pathlib import Path

# 마운트시킨 데이터 셋 위치
data_path = '/content/gdrive/MyDrive/dataset'
classes = [path for path in Path(data_path).iterdir() if path.is_dir()]
classes

classes 출력 결과는 아래처럼 보일 것입니다.

classes 출력 결과

위의 코드에서 내가 가진 데이터 셋은 예시로 보여주기엔 클래스가 너무 많습니다. 그래서 저는 2개의 클래스만 사용하도록 하겠습니다. 아래의 코드는 우리가 가진 클래스(디렉터리 이름과 동일)의 모든 파일들의 경로를 가져옵니다.

classes = classes[:2]
classes

자 이제 우리에게는 ‘ardha matsyendrasana’, ‘agnistambhasana’ 두 개의 클래스만 남았습니다.

두 클래스의 파일 경로를 모두 불러올 것입니다.

files = []
for cls in classes:
  files += 
files

Inference 해서 json으로 저장하기

아래는 TensorFlow 에서 제공하는 코드를 기반으로 17개의 body key points만을 리턴하도록 수정한 코드입니다.

def get_keypoints(image, 
                  keypoints_with_scores,
                  output_image_height=None, 
                  keypoint_threshold=0.0):
    height, width, channel = image.shape
    aspect_ratio = float(width) / height

    keypoints_all = []
    num_instances,_,_,_ = keypoints_with_scores.shape
    for id in range(num_instances):
        kpts_x = keypoints_with_scores[0,id,:,1]
        kpts_y = keypoints_with_scores[0,id,:,0]
        kpts_scores = keypoints_with_scores[0,id,:,2]
        kpts_abs_xy = np.stack(
            [width*np.array(kpts_x),height*np.array(kpts_y)],axis=-1)
        kpts_above_thrs_abs = kpts_abs_xy[kpts_scores > keypoint_threshold,: ]
        keypoints_all.append(kpts_above_thrs_abs)

    return np.concatenate(keypoints_all,axis=0)

자, 이제! file들을 각각 inference해서 {'file path' : (17, 2) 배열} 상태로 dictionary에 저장해봅시다!

import json

keypoints = {}

for image_path in files:
    # 이미지 에러에 대한 처리
    # 이미지가 tf.io.read_file로 읽을 수 없는 타입인 경우에 대비
    try:
        image = tf.io.read_file(image_path)
        image = tf.image.decode_jpeg(image)
    except:
        print('image error : ', image_path)
        continue

    input_image = tf.expand_dims(image, axis=0)
    input_image = tf.image.resize_with_pad(input_image, input_size, input_size)

    # 모델 인퍼런스 에러에 대한 처리
    try:
        keypoints_with_scores = movenet(input_image)
    except:
        print('model error : ', image_path)
        continue

    display_image = tf.expand_dims(image, axis=0)
    display_image = tf.cast(tf.image.resize_with_pad(
        display_image, 224, 224), dtype=tf.int32)
    output_overlay = get_keypoints(np.squeeze(display_image.numpy(), axis=0), 
                                  keypoints_with_scores)
    
    keypoints.setdefault('/'.join(image_path.split('/')[3:]), output_overlay.tolist())
    
keypoints

위에서 keypoints dictionary를 확인해보니 잘 저장이 된 것 같네요! 이제 json 파일로 저장해봅시다. 아래의 명령어를 실행해보면 /content/keypoints.json 이라는 json 파일이 생길 것입니다.

json 파일 저장하기

[그림 5] json 파일 저장하기

with open("./keypoints.json", "w") as json_file: 
    json.dump(keypoints, json_file)

이 json 파일을 마우스 우클릭으로 저장할 수도 있지만 아래의 명령어를 사용하면 코드를 사용하여 내 로컬 PC에 파일을 저장할 수 있습니다!

from google.colab import files
files.download("./keypoints.json")

이미지 데이터와 MoveNet keypoints 데이터를 로드하는 custom dataloader 만들기

데이터를 만들었으니 이제 기존에 가지고 있던 이미지만 로드하는 데이터로더가 아닌 이미지와 keypoints 모두를 로드하는 데이터로더가 필요합니다.
우리의 새 데이터로더는 image, label 형태가 아닌 {"input_1": img, "input_2": keypoint}, label로 데이터를 로드할 것입니다!

import os
import pathlib

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

import json

with open("./keypoints.json", "r") as json_file:
    keypoint_dict = json.load(json_file)

new_keypoint_dict = {}
for key in keypoint_dict.keys():
    new_key = '/'.join(key.split('/')[-2:])
    new_val = keypoint_dict[key]
    new_keypoint_dict[new_key] = new_val

del keypoint_dict
keypoint_dict = new_keypoint_dict
del new_keypoint_dict

def process_keypoint(file_path):
    file_path =  '/'.join(file_path.numpy().decode('utf-8').split('/')[-2:])
    # keypoint = np.array(keypoint_dict[file_path])
    keypoint = tf.convert_to_tensor(keypoint_dict[file_path], dtype=tf.float32)
    return keypoint

def process_path(file_path, class_names, img_shape=(224, 224)):
    label = tf.strings.split(file_path, os.path.sep)
    label = label[-2] == class_names
    label = tf.cast(label, tf.float32)

    img = tf.io.read_file(file_path)
    img = tf.image.decode_png(img, channels=3)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, img_shape) 
    # img = img / 255.0

    [keypoint,] = tf.py_function(process_keypoint, [file_path], [tf.float32])

    return {"input_1": img, "input_2": keypoint}, label


def load_label(label_path):
    class_names = []
    with open(label_path) as f:
        for line in f:
            line = line.strip()
            class_names.append(line)

    return np.array(class_names)


def get_spilt_data(ds, ds_size, train_split=0.8, val_split=0.1, test_split=0.1, shuffle=True, shuffle_size=1000):
    assert (train_split + val_split) == 1

    if shuffle:
        ds = ds.shuffle(shuffle_size, seed=12)

    train_size = int(train_split * ds_size)
    val_size = int(val_split * ds_size)

    train_ds = ds.take(train_size)
    val_ds = ds.skip(train_size).take(val_size)

    return train_ds, val_ds


def augment(inputs, label):
    image, keypoint = inputs['input_1'], inputs['input_2']
    image = tf.image.random_crop(image, size=[224, 224, 3])
    image = tf.image.adjust_brightness(image, 0.4)
    image = tf.image.random_brightness(image, max_delta=0.4)
    return {'input_1' : image, 'input_2' : keypoint}, label


def prepare_for_training(ds, batch_size=32, cache=True, training=True):
    if cache:
        if isinstance(cache, str):
            ds = ds.cache(cache)
        else:
            ds = ds.cache()

    ds = ds.repeat()
    if training:
        ds = ds.map(lambda x, y: augment(x, y))
    ds = ds.batch(batch_size)
    ds = ds.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)

    return ds


def load_data(data_path, img_shape, batch_size=32, is_train=True):
    class_names = [cls for cls in os.listdir(data_path) if cls != '.DS_Store']
    data_dir = pathlib.Path(data_path)
    list_ds = tf.data.Dataset.list_files(str(data_dir / '*/*'))

    labeled_ds = list_ds.map(lambda x: process_path(x, class_names, img_shape))
    labeled_ds = prepare_for_training(labeled_ds, batch_size=batch_size, training=is_train)

    DATASET_SIZE = tf.data.experimental.cardinality(list_ds).numpy()

    return labeled_ds, DATASET_SIZE

코드에 대한 약간의 설명이 필요할 것 같습니다.

def load_data(data_path, img_shape, batch_size=32, is_train=True):
    class_names = [cls for cls in os.listdir(data_path) if cls != '.DS_Store']
    data_dir = pathlib.Path(data_path)
    list_ds = tf.data.Dataset.list_files(str(data_dir / '*/*'))

    labeled_ds = list_ds.map(lambda x: process_path(x, class_names, img_shape))
    labeled_ds = prepare_for_training(labeled_ds, batch_size=batch_size, training=is_train)

    DATASET_SIZE = tf.data.experimental.cardinality(list_ds).numpy()

    return labeled_ds, DATASET_SIZE

load_data의 역할은 라벨 로드, 이미지 로드, 데이터 배치 만들기 등의 역할을 하는 함수를 호출하여 최종으로 받을 데이터를 만드는 역할을 합니다. load_data에서 호출하는 함수들을 차례로 확인해봅시다.

이전처럼 single input을 사용하는 모델이라면 prcoess_path에서 img, label을 return을 해주었을 것입니다. 하지만 지금은 들어오는 img뿐만 아니라 keypoints도 가져와야 합니다. process_keypoint 함수를 이용하여 keypoint를 가져온 후 img, keypoint, label을 리턴해줍니다.

def process_keypoint(file_path):
    file_path =  '/'.join(file_path.numpy().decode('utf-8').split('/')[-2:])
    # keypoint = np.array(keypoint_dict[file_path])
    keypoint = tf.convert_to_tensor(keypoint_dict[file_path], dtype=tf.float32)
    return keypoint

def process_path(file_path, class_names, img_shape=(224, 224)):
    label = tf.strings.split(file_path, os.path.sep)
    label = label[-2] == class_names
    label = tf.cast(label, tf.float32)

    img = tf.io.read_file(file_path)
    img = tf.image.decode_png(img, channels=3)
    img = tf.image.convert_image_dtype(img, tf.float32)
    img = tf.image.resize(img, img_shape) 
    # img = img / 255.0

    [keypoint,] = tf.py_function(process_keypoint, [file_path], [tf.float32])

    return {"input_1": img, "input_2": keypoint}, label

prepare_for_training에서 augmentation 해줄 때도 image, keypoint = inputs['input_1'], inputs['input_2']를 통해 image를 가져와서 image만 augmentation 해주어야 합니다. 이때 주의할 점은, 우리는 keypoints를 사용할 것이기 때문에 좌우 flip과 같은 augmentation은 하지 않도록 해야 합니다.

def augment(inputs, label):
    image, keypoint = inputs['input_1'], inputs['input_2']
    image = tf.image.random_crop(image, size=[224, 224, 3])
    image = tf.image.adjust_brightness(image, 0.4)
    image = tf.image.random_brightness(image, max_delta=0.4)
    return {'input_1' : image, 'input_2' : keypoint}, label


def prepare_for_training(ds, batch_size=32, cache=True, training=True):
    if cache:
        if isinstance(cache, str):
            ds = ds.cache(cache)
        else:
            ds = ds.cache()

    ds = ds.repeat()
    if training:
        ds = ds.map(lambda x, y: augment(x, y))
    ds = ds.batch(batch_size)
    ds = ds.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)

    return ds

자 이제 데이터를 잘 가져오는지 확인해볼까요?
아까 우리는 2개의 클래스 'ardha matsyendrasana', 'agnistambhasana' 대해서만 json 파일을 만들었으니 그 점 유의하세요!

# 내 데이터 위치
train_data_path = '/content/dataset'

train_ds, train_size = load_data(data_path=train_data_path, img_shape=(224, 224), batch_size=1)

for inputs, label in train_ds.take(1):
    print(inputs)
    print(label)

오! 아주 로드가 잘되고 있습니다. 자 이제 입력이 두 개인 multiple input을 가진 모델로 바꿔보겠습니다!

Multi-input 모델 만들기

import tensorflow as tf
from tensorflow.keras.applications import EfficientNetB0
from tensorflow.keras.applications import VGG16

class YogaPose(tf.keras.Model):
    def __init__(self, num_classes=30, freeze=False):
        super(YogaPose, self).__init__()
        self.base_model = EfficientNetB0(include_top=False, weights='imagenet')
        self.keypoint = tf.keras.Sequential([tf.keras.layers.Flatten(input_shape=(17, 2)),
                                              tf.keras.layers.Dense(34),])
        
        if freeze:
            self.base_model.trainable = False

        self.top = tf.keras.Sequential([tf.keras.layers.GlobalAveragePooling2D(name="avg_pool"),
                                        tf.keras.layers.BatchNormalization(),
                                        tf.keras.layers.Dropout(0.6, name="top_dropout")])
        
        self.concat = tf.keras.layers.Concatenate(axis=-1)
        self.classifier = tf.keras.layers.Dense(num_classes, activation="softmax", name="pred")

    def call(self, inputs, training=True):
        image, keypoint = inputs['input_1'], inputs['input_2']
        x1 = self.base_model(image)
        x1 = self.top(x1)
        x2 = self.keypoint(keypoint)
        x = self.concat([x1, x2])
        x = self.classifier(x)
        return x

call function을 통해 input이 들어오게 되겠죠?
우리의 dataloader가 리턴해주는 데이터의 형태는 {"input_1": img, "input_2": keypoint}, label이기 때문에 image, keypoint = inputs['input_1'], inputs['input_2'] 코드처럼 image, keypoint 각각을 저장해야 합니다.

def call(self, inputs, training=True):
    image, keypoint = inputs['input_1'], inputs['input_2']
    x1 = self.base_model(image)
    x1 = self.top(x1)
    x2 = self.keypoint(keypoint)
    x = self.concat([x1, x2])
    x = self.classifier(x)
    return x

그리고 위의 코드처럼 각각의 입력을 받아줄 레이어도 지정해줍니다.
image를 받을 레이어는 self.base_modelkeypoint를 받을 레이어는 self.keypoint입니다.

자 이제 모델이 어떤 input을 넣었을 때 output이 잘 나오는지 볼까요?

# 가짜 input
inputs = {'input_1':tf.ones([1, 224, 224, 3]), 'input_2':tf.ones([1, 17, 2])}
model = YogaPose(num_classes=2, freeze=True)
model(inputs)

값이 잘 나오네요!

오예

[그림 6] 오예 🎉

이제 모델을 학습해서 결과를 보는 일만 남았네요. 그 일은 model.fit()에서 벗어나기! (2)에서 계속 진행해보겠습니다. 해당 콘텐츠는 피어리뷰 이후에 약간의 수정을 거쳐 코크리 블로그에 올라올 예정입니다! 기대해주세요.

참고자료

콘텐츠 크리에이터 소개

안녕하세요! 여행, 피자, 역사를 좋아하는 머신러닝 엔지니어 박정현(Eden)입니다. 코로나를 뚫고 이탈리아(피자와 역사는 덤)를 다녀왔을 정도로 여행을 좋아합니다. 좋은 콘텐츠 많이 보여드렸으면 좋겠습니다. (블로그)