본문 바로가기
DL|ML

model.fit()에서 벗어나기! (2)

by 이든Eden 2022. 4. 20.
모두의 연구소에서 진행하는
"함께 콘텐츠를 제작하는 콘텐츠 크리에이터 모임"
COCRE(코크리) 1기 회원으로 제작한 글입니다. 
코크리란? 🐘

 

model.fit()에서 벗어나기! (1) 요약

지난편에서 우리의 몇 가지에 대해 이야기했습니다

  • tf.data를 이용한 데이터 로드하기
  • tf.keras.Model을 상속받아 나만의 모델 만들기
  • Train loop로 모델 학습하기

하지만 우리가 만들었단 간단한 train loop 로는 지금 어느정도 학습되고 있는지, loss가 어느 정도 인지를 알 수 없고 모델을 저장해보지도 않았습니다. 오늘은 그것들을 해보려고 합니다! 그리고 추가로 argparse를 이용하여 epoch, batch 사이즈 등의 값을 읽어오도록 해보겠습니다. 불태워봅시다.

 

 

[그림 1] 🔥🔥

 

 

이 기능들을 추가하면서 우리는 조각코드를 계속 보게될 것입니다. 조각코드가 아닌 전체의 흐름을 보고싶다면 아래의 github에서 전체 코드를 살펴보는 것을 추천합니다!

 

https://github.com/parkjh688/bye_model.fit

 

GitHub - parkjh688/bye_model.fit: Let's train deep learning model with Trainer instead of model.fit

Let's train deep learning model with Trainer instead of model.fit - GitHub - parkjh688/bye_model.fit: Let's train deep learning model with Trainer instead of model.fit

github.com

 

 

Train loop에 기능추가하기

1. Progbar 추가하기

Progbar란 Progression Bar의 줄임말로 아래 [그림 1] 같은 바형태로 얼마나 진행되었는지 보여주는 것입니다.

 

[그림 2] Progress Bar

이전 Trainer의 train() 함수는train_dataset, train_metric 단 두 개만을 인자로 받았습니다. 이제는 각 step 별로 Progbar를 찍어주기 위해 steps_per_epoch이라는 인자를 추가해줄 것입니다. 여기서 step이란 1개의 batch의 loss를 계산하고 gradient를 한 번 업데이트하는 것을 말합니다. 그 step은 어떻게 구할 수 있을까요? 전체 학습 데이터 사이즈를 TRAIN_SIZE라고 했을 때 그걸 batch 사이즈 만큼으로 나눈게 step수가 되겠죠? 코드로 바꾼다면 우리는 아래의 labmda compute_steps_per_epoch으로 계산할 수 있을 것입니다. 

 

compute_steps_per_epoch = lambda x: int(math.ceil(1. * x / batch_size))
steps_per_epoch = compute_steps_per_epoch(TRAIN_SIZE)

 

 

하지만 우리는 아직 TRAIN_SIZE를 모릅니다. 그래서 기존의 load_data에서 데이터를 가져올 때 TRAIN_SIZE를 함께 계산하여 가져오게 할 것입니다. 바로 이렇게 말이죠!

train_ds, TRAIN_SIZE = load_data(data_path=train_path, img_shape=(224, 224), batch_size=batch_size)

 

그렇다면 load_data는 어떻게 바뀌어야할까요? 아래의 코드를 보세요. DATASET_SIZE를 구하는 코드가 생겼습니다. tf.data.experimental.cardinality()로 우리는 데이터셋의 사이즈를 알 수 있습니다. 그리고 .numpy() 메소드를 호출하여 넘파이 배열로 바꾸었습니다.

def load_data(data_path, img_shape, batch_size=64, 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

 

이제 step 사이즈도 알았으니 step 마다 진행사항을 찍어줄 수 있을 것 같습니다! 진행사항을 찍기 위해 from tensorflow.keras.utils import Progbar를 사용해보겠습니다. 우리가 원하는 Progbar의 전체 길이는 steps_per_epoch(이전에 구한 DATA_SIZE와 같음) x batch가 될 것 입니다. 1 step 마다 1 batch를 사용하게 되니까요. 휴 헷갈리죠? 😵  Progbar는 아래처럼 만들면 됩니다.

 

metrics_names = ['train_loss']
progBar = Progbar(steps_per_epoch * self.batch, stateful_metrics=metrics_names)

 

그리고 매 step이 바뀔 때 마다 update 함수를 사용해주면 됩니다. values에는 progbar에서 보여주고 싶은 값을 넣어주면 됩니다.

values = [('train_loss', train_loss), ('train_acc', train_acc)]
progBar.update((step_train + 1) * self.batch, values=values)

 

하지만 Progbar를 사용하지 않고 tqdm, 커스텀 progress bar 등 어떤 것으로 만들어도 상관없습니다.

 

2. Validation 데이터 추가하기

우리는 지금까지 train 데이터로만 진행을 해왔습니다. 하지만 validation 데이터가 있는 상황도 있을 것입니다. 그리고 validation 데이터 추가를 했다고해서 크게 코드에 차이가 있지 않습니다. 그래서 빠르게 validation 데이터를 추가해보도록 하겠습니다.

 

저는 validation 데이터가 따로 있다고 가정하고 만들었습니다. 그렇다는건 train_path, val_path가 모두 존재한다는 뜻이겠죠! 단순히 load_data를 두 번 해주는 것에 지나치지 않습니다. 아래와 같습니다.

train_ds, TRAIN_SIZE = load_data(data_path=train_path, img_shape=(224, 224), batch_size=batch_size)
val_ds, VAL_SIZE = load_data(data_path=val_path, img_shape=(224, 224), batch_size=batch_size, is_train=False)

compute_steps_per_epoch = lambda x: int(math.ceil(1. * x / batch_size))
steps_per_epoch = compute_steps_per_epoch(TRAIN_SIZE)
val_steps = compute_steps_per_epoch(VAL_SIZE)

 

하지만 이 데이터를 train step에 넣어주려면 어떻게 해야될까요? 당연히 Trainer 클래스의 train 함수에 인자를 늘려줘야겠죠. 그리고 validation을 위한 Progbar 역시 만들어주도록 해봅시다. 오! 슬슬 복잡해지는 것처럼 보입니다. 그럴 땐 튜토리얼을 안읽고 github 클론이나 받아서 코드나 돌려볼까 하는 마음이 생기죠 

for step, (x_batch_val, y_batch_val) in enumerate(val_dataset):
    logits = model(x_batch_val, training=False)
    val_loss = self.loss_fn(y_batch_val, logits)
    
    # Update val metrics
    val_acc = self.compute_acc(logits, y_batch_val)
    values = [('train_loss', train_loss), ('train_acc', train_acc), ('val_loss', val_loss), ('val_acc', val_acc)]

progBar.update((step_train + 1) * self.batch, values=values, finalize=True)

 

그럼 아래 [그림 2]와 같은 Progbar 결과를 얻을 수 있을 것입니다. 

[그림 3]

 

사실 이 Progbar를 그리는데에 약간의.. anger가 있었습니다. Progbar의 버그 때문인데요. 하지만 오아시스 행님들도 Don't look back in anger라고 노래하셨으니 마음을 비우며 혹시나 같은 에러가 나는 분들을 위해 제가 링크를 하나 달아놓겠습니다.

https://github.com/keras-team/keras/issues/5906

 

Keras Progbar Logger is not working as expected · Issue #5906 · keras-team/keras

When using model.fit_generator, the progress-bar does not work as expected. As noted on StackOverflow, the following verbosity levels exist (which should be explained in the documentation): 0: No l...

github.com

 

제가 위에서 겪은 버그를 살짝 이야기해드리고 싶은데요. 아래의 [그림 4]처럼 매번 뉴라인을 찍어주는것이 버그였습니다. 물론 버전에 따라 다를 수 있습니다. 이와 같은 버그를 겪는 분들은 케라스를 인스톨해둔 위치에 찾아가 keras/utils/generic_utils.py를 한 번 들여다보는 것을 추천합니다. 출력하는 부분에서 문제가 있을지도 모릅니다.

[그림 4]

 

3. 모델 저장하기

이제 학습과 관련하여 몇 가지를 해놓았으니 학습된 모델을 저장해보도록 합시다. 기쁘게도 tensorflow에는 tf.train.Checkpoint라는게 있습니다. 모델을 저장하게 해주는 아주 친절한 친구입니다. 사용법은 아주 간단합니다. 쓰려고 미리 준비해뒀던 optimizer와 model을 인자로 넣어주고 checkpoint를 만든 뒤 그 checkpoint를 checkpoint manager로 만들면 됩니다. 이 때 directory는 모델이 저장될 directory, max_to_keep은 최대 몇 개를 저장할지 개수를 지정해주는 것입니다.

model = YogaPose(num_classes=num_classes)
optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001)

checkpoint = tf.train.Checkpoint(optimizer=optimizer, model=model)
manager = tf.train.CheckpointManager(checkpoint, directory=".", max_to_keep=5)

 

그 다음엔 역시 Trainer 클래스의 train 함수에 보내 1 epoch이 끝날 때마다 manager.save()를 넣어 모델을 저장해주면 됩니다. 하지만 실제 tensorflow, keras에는 callback이라는게 있죠. 그 callback 중엔 학습한 모델 중 가장 좋은 모델만 저장해주는 애가 있습니다. 우리도 그것과 비스무리한걸 구현해볼까 합니다.

 

아래를 보세요. 아! train 부분은 train_on_batch 함수로 그대로 빼뒀습니다. 그 점 참고하세요. 가장 좋은 모델을 저장하려면 임의의 loss인 best_loss를 두고 매번 생기는 val_loss가 그것보다 낮을 때만 모델을 저장하게 하면 됩니다. loss는 0에 가까워질 수록 좋으니까요. 다만 loss가 아닌 acc로 해도 무방합니다.

 

def compute_acc(self, y_pred, y):
    correct = tf.equal(tf.argmax(y_pred, 1), tf.argmax(y, 1))
    accuracy = tf.reduce_mean(tf.cast(correct, tf.float32))
    return 
        
@tf.function
def train_on_batch(self, x_batch_train, y_batch_train):
    with tf.GradientTape() as tape:
        logits = model(x_batch_train, training=True)    # 모델이 예측한 결과
        train_loss = self.loss_fn(y_batch_train, logits)     # 모델이 예측한 결과와 GT를 이용한 loss 계산

    grads = tape.gradient(train_loss, model.trainable_weights)  # gradient 계산
    self.optimizer.apply_gradients(zip(grads, model.trainable_weights))  # Otimizer에게 처리된 그라데이션 적용을 요청

    return train_loss, logits

def train(self, train_dataset, acc_metric, steps_per_epoch, val_dataset, val_step, checkpoint_manager):
    metrics_names = ['train_loss', 'train_acc', 'val_loss']

    best_loss = 100
    for epoch in range(self.epochs):
        print("\nEpoch {}/{}".format(epoch+1, self.epochs))

        train_dataset = train_dataset.shuffle(100)
        val_dataset = val_dataset.shuffle(100)

        train_dataset = train_dataset.take(steps_per_epoch)
        val_dataset = val_dataset.take(val_step)

        progBar = Progbar(steps_per_epoch * self.batch, stateful_metrics=metrics_names)

        train_loss, val_loss = 100, 100

        # 데이터 집합의 배치에 대해 반복합니다
        for step_train, (x_batch_train, y_batch_train) in enumerate(train_dataset):
            train_loss, logits = self.train_on_batch(x_batch_train, y_batch_train)

            # train metric(mean, auc, accuracy 등) 업데이트
            acc_metric.update_state(y_batch_train, logits)

            train_acc = self.compute_acc(logits, y_batch_train)
            values = [('train_loss', train_loss), ('train_acc', train_acc)]
            # print('{}'.format((step_train + 1) * self.batch))
            progBar.update((step_train + 1) * self.batch, values=values)

        for step, (x_batch_val, y_batch_val) in enumerate(val_dataset):
            logits = model(x_batch_val, training=False)
            val_loss = self.loss_fn(y_batch_val, logits)
            val_acc = self.compute_acc(logits, y_batch_val)
            values = [('train_loss', train_loss), ('train_acc', train_acc), ('val_loss', val_loss), ('val_acc', val_acc)]
        progBar.update((step_train + 1) * self.batch, values=values, finalize=True)

        if val_loss < best_loss:
            best_loss = val_loss
            print("\nSave better model")
            print(checkpoint_manager.save())

 

아래 [그림 3]을 보세요. loss가 더 적어졌을 때는 모델을 저장하고 아닐 때는 하지 않고 있습니다. 우리만의 작은 callback을 만든셈이죠! 드디어 끝이 보입니다.

 

[그림 5]

 

여기서 잠깐.

 

[그림 6]

 

여기까지 예제 코드를 잘 만들어왔던 제가 사실 난관에 봉착했었죠. 사실 난관은 잦았지만.. 어쨌든 Yoga Pose 이미지 데이터만으로는 모델이 생각보다 학습이 잘안된다는 것이었습니다. 심지어 오버피팅도 안됐다구요! 그래서 제가 시도한 한 가지가 있었습니다.

 

간단히 설명한다면 우리가 쓰고 있는 요가 데이터는 [그림 7]의 왼쪽 이미지와 같이 생겼습니다. 학습에서의 문제는 오버피팅조차 되지 않고 모델이 학습하기 싫어한다는 것이었습니다. 어쩌면 주인을 닮았을지도 모릅니다. 저도 공부하기 싫거든요. 그래서 [그림 7]의 오른쪽 이미지 같이 자세의 keypoints들을 모델에 함께 넣어주기로 했습니다. 모델에게 이미지 외에도 조금 더 많은 정보를 주는 것이죠. 그러기 위해서는 제가 가진 Yoga Pose 이미지 데이터에 있는 동작들에 대한 keypoints 값을 가지고 있어야합니다.

 

이 과정은 직접 annotation 할 수도 있었지만 그것은 하고싶지 않았기에.. 이미 학습되어있는 MoveNet이라는 모델로부터 keypoints label을 얻는 것으로 하였습니다. 물론, 모델을 학습하기 위해서는 정확한 label이 있는 것은 중요합니다. 하지만 keypoints를 넣어주면 모델이 좀 더 잘 학습하지않을까? 정도 갑자기 생각난 아이디어를 체크해보기 위해서는 아주 정교한 fine label이 아닌 대충 만들어본 coarse label로도 결과를 체크해볼 수 있을 것입니다. 그 과정에서 더 많은 데이터를 가져와야하기 때문에 load_data도 바뀌었고, 입력이 늘었으니 Model도 바뀌었습니다. 그 모든 과정들은 아래의 코랩을 참고하시길 바랍니다.

[그림 7]

 

https://colab.research.google.com/drive/1jzsSh4Ug42LtroOmnZo0MgE7BNMCkaHH?hl=ko#scrollTo=VHmTwACwFW-v 

 

Make coarse label and custom dataloader with MoveNet.ipynb

Colaboratory notebook

colab.research.google.com

 

4.  argparse 사용하기

이제 마지막으로 argparse라는 것을 추가해보겠습니다. 없어도 되는 기능이지만 이걸 넣으면 왠지 멋있어보이니까 그냥 하겠습니다. 멋있는 것도 중요하지만, argparse를 사용하면 매 번 .py 내에서 값을 바꿔주지 않고 python train.py 명령으로 실험에 필요한 epoch, batch, data path 등을 설정해줄 수 있습니다! 그 동안은 아래의 코드와 같이 main 을 넣어 학습을 시켰습니다. 하지만 이제는 num_classes, epoch, batch_size, data_path 등을 파이썬 실행시 받을 수 있도록 바꿔보겠습니다.

if __name__ == '__main__':
    num_classes = 107
    epoch = 1000
    batch_size = 64

    train_path = '/Users/edensuperb/Downloads/pythonProject/dataset'
    val_path = '/Users/edensuperb/Downloads/pythonProject/dataset'

    train_ds, TRAIN_SIZE = load_data(data_path=train_path, img_shape=(224, 224), batch_size=batch_size)
    val_ds, VAL_SIZE = load_data(data_path=val_path, img_shape=(224, 224), batch_size=batch_size, is_train=False)

 

아래와 같이 만들면 될 것 입니다.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--epoch",          type=int,       default=100)
parser.add_argument("--batch_size",     type=int,       default=64)
parser.add_argument("--num_classes",    type=int,       default=107)
parser.add_argument("--img_size",       type=int,       default=224)
parser.add_argument("--train_path",     type=str,		default='./dataset/train')
parser.add_argument("--val_path",       type=str,		default='./dataset/val')
parser.add_argument("--checkpoint_path",type=str,		default='./checkpoints')

args = parser.parse_args()

 

후에 받은 인자들은 args.batch_size 와 같이 사용하면 됩니다. 짜잔 python train.py --batch_size 32 --checkpoint_path '.' 로 했더니 원하는대로 잘 되었습니다.

[그림 8]

 

 

5. 모델 테스트해보기

[그림 9]

제가 잊고있던 것이 있었습니다. 바로 모델 테스트입니다. 이게 진짜 진짜 마지막입니다. 학습한 모델이 잘 테스트 되는지 역시도 중요하죠. 짧은 코드와 함께 해보겠습니다. test.py라는 새 파일에 역시 argparse로 인자를 받아보겠습니다. 방법은 간단합니다 tf.train.Checkpoint로 이미 저장된 ckpt를 가져오면 됩니다. ckpt에는 우리가 만든 모델이 그대로 저장되어 있기 때문입니다. 그 후 다시 데이터를 로드한 후 inference해서 진짜 label과 inference 결과를 비교하여 몇 개나 맞았는지 체크해보면 되죠. 아래는 그 과정입니다.

 

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--epoch",          type=int,       default=100)
    parser.add_argument("--num_classes",    type=int,       default=107)
    parser.add_argument("--img_size",       type=int,       default=224)
    parser.add_argument("--test_path",      type=str,       default='./dataset/test')
    parser.add_argument("--checkpoint_path",type=str,		default='./checkpoints/ckpt-77')

    args = parser.parse_args()

    model = YogaPose(num_classes=args.num_classes)
    optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001)

    test, TEST_SIZE = load_data(data_path=args.test_path, img_shape=(args.img_size, args.img_size), batch_size=64)

    checkpoint = tf.train.Checkpoint(optimizer=optimizer, model=model)
    checkpoint.restore(args.checkpoint_path)

    for step_train, (x_batch_train, y_batch_train) in enumerate(test.take(10)):
        # print(model(x_batch_train))
        prediction = model(x_batch_train)
        # print(tf.argmax(y_batch_train, axis=1))
        # print(tf.argmax(prediction, axis=1))
        # print(tf.equal(tf.argmax(y_batch_train, axis=1), tf.argmax(prediction, axis=1)))
        print("{}/{}".format(np.array(tf.equal(tf.argmax(y_batch_train, axis=1), tf.argmax(prediction, axis=1))).sum(), tf.argmax(y_batch_train, axis=1).shape[0]))
        # print("Prediction: {}".format(tf.argmax(prediction, axis=1)))

 

 

아래와 같이 나오네요. 대충 32개 중에 23개 - 28개 사이로 맞추고 있습니다.

[그림 10]

 

 

이제 개인적으로 원하는 데이터를 로드해서 train loop로 학습하고 test까지 해볼 수 있을 것 같다고 생각이 듭니다. 글이 마음에 드셨을지 모르겠습니다. 하지만 저는 약간.. 터프 타임이었네요. 저는 또 다른 콘텐츠로 돌아오겠습니다. 끝까지 읽어주셔서 감사드립니다. 

 

[그림 11]

 

이미지 출처

[그림 1] https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sanjizoro1&logNo=220920594741 

[그림 2] https://365psd.com/wp-content/uploads/2011/07/loading.jpg

[그림 6] https://www.memesmonkey.com/images/memesmonkey/a1/a13ea25492e014a4ccb726595b4cf249.jpeg

[그림 7] https://han.gl/YSqIN

[그림 9] https://i3.ruliweb.com/img/20/05/30/1726481ada12392f3.jpg

[그림 11] http://t1.daumcdn.net/liveboard/ziksir/5480a06540804ea2b22dcc294a109606.jpg