Introduction

This notebook presents Keypoint Detection using a CNN on the CAT dataset.

Resources

Imports

In [1]:
import os
import glob
import numpy as np
import matplotlib.pyplot as plt
import PIL
import PIL.ImageDraw

Limit TensorFlow GPU memory usage

In [2]:
import tensorflow as tf
gpu_options = tf.GPUOptions(allow_growth=True)  # init TF ...
config=tf.ConfigProto(gpu_options=gpu_options)  # w/o taking ...
with tf.Session(config=config): pass            # all GPU memory

Configuration

Point this to dataset directory, folder should contain CAT_00, CAT_01 and so on.

In [3]:
dataset_location = '/home/marcin/Datasets/cat-dataset/cats/'

Helpers

Helper to draw keypoints on the image

In [4]:
def draw_keypoints(img, keypoints, r=2, c='red'):
    """Draw keypoints on PIL image"""
    draw = PIL.ImageDraw.Draw(img)
    for x, y in keypoints:
        draw.ellipse([x-r, y-r, x+r, y+r], c)
    return img
In [5]:
def plot_images(indices, images, keypoints):
    """Plot bunch of images with keypoint overlay
    
    Params:
        indices - indices of images to plot, e.g. [0, 10, 20, ...]
        images - np.ndarray with raw images, [batch_size, width, height, channel]
        keypoints - np.ndarray with keypoints, [batch_size, num_keypoints, x, y]
    """
    _, axes = plt.subplots(nrows=1, ncols=len(indices), figsize=[12,4])
    if len(indices) == 1: axes = [axes]

    for i, idx in enumerate(indices):
        img = PIL.Image.fromarray(images[idx])
        kps = keypoints[idx]
        axes[i].imshow(draw_keypoints(img, kps))
        axes[i].axis('off')
    
    plt.show()

Load Dataset

In this section we:

  • load images and keypoints from folder structure
  • resize to 224x224 and save into numpy .npz file

Subfolders within dataset

In [6]:
folders_all = ['CAT_00', 'CAT_01', 'CAT_02', 'CAT_03', 'CAT_04', 'CAT_05', 'CAT_06']

Get paths to all images

In [7]:
def build_image_files_list(folders):
    image_files_list = []
    for folder in folders:
        wild_path = os.path.join(dataset_location, folder, '*.jpg')
        image_files_list.extend(sorted(glob.glob(wild_path)))
    return image_files_list
In [8]:
image_paths_all = build_image_files_list(folders_all)
In [9]:
print('Nb images:', len(image_paths_all))
image_paths_all[:3]
Nb images: 9997
Out[9]:
['/home/marcin/Datasets/cat-dataset/cats/CAT_00/00000001_000.jpg',
 '/home/marcin/Datasets/cat-dataset/cats/CAT_00/00000001_005.jpg',
 '/home/marcin/Datasets/cat-dataset/cats/CAT_00/00000001_008.jpg']

Helper to load keypoint data from .cat files

In [10]:
def load_keypoints(path):    
    """Load keypoints from .cat file
    
    The .cat file is a single-line text file in format: 'nb_keypoints x1, y1, x2, y2, ...'
    """
    with open(path, 'r') as f:
        line = f.read().split()  # [nb_keypoints, x1, y1, x2, y2, ...]
    keypoints_nb = int(line[0])  # int
    keypoints_1d = np.array(line[1:], dtype=int)  # np.ndarray, [x1, y1, x2, y2, ...]
    keypoints_xy = keypoints_1d.reshape((-1, 2))  # np.ndarray, [[x1, y1], [x2, y2], ...]
    assert keypoints_nb == len(keypoints_xy)
    assert keypoints_nb == 9                # always nine keypoints, eyes, nose, two ears
    return keypoints_xy                     # np.ndarray, [[x1, y1], [x2, y2], ...]

Open single image and load corresponding keypoints

In [11]:
example_path = image_paths_all[0]
img = PIL.Image.open(example_path)
kps = load_keypoints(example_path+'.cat')

Show example keypoints

In [12]:
display(kps)
array([[175, 160],
       [239, 162],
       [199, 199],
       [149, 121],
       [137,  78],
       [166,  93],
       [281, 101],
       [312,  96],
       [296, 133]])

Show example image

In [13]:
display(draw_keypoints(img.copy(), kps))

Helper to scale image and keypoints

In [14]:
def scale_img_kps(image, keypoints, target_size):
    width, height = image.size
    ratio_w = width / target_size
    ratio_h = height / target_size
    
    image_new = image.resize((target_size, target_size), resample=PIL.Image.LANCZOS)
    
    keypoints_new = np.zeros_like(keypoints)
    keypoints_new[range(len(keypoints_new)), 0] = keypoints[:,0] / ratio_w
    keypoints_new[range(len(keypoints_new)), 1] = keypoints[:,1] / ratio_h
    
    return image_new, keypoints_new

Test it

In [15]:
img2, kps2 = scale_img_kps(img, kps, target_size=224)
display(draw_keypoints(img2.copy(), kps2))

Helper to load and transform both input image and keypoints

In [16]:
def load_image_keypoints(image_path, keypoints_path, target_size):
    image = PIL.Image.open(image_path)
    keypoints = load_keypoints(keypoints_path)
    image_new, keypoints_new = scale_img_kps(image, keypoints, target_size)
    return image, keypoints, image_new, keypoints_new

Show couple more examples

In [17]:
idx = 21

image, keypoints, image_new, keypoints_new = load_image_keypoints(
    image_paths_all[idx], image_paths_all[idx]+'.cat', target_size=224)
display(draw_keypoints(image.copy(), keypoints))
display(draw_keypoints(image_new.copy(), keypoints_new))

Custom Generator

In [18]:
class CatSequence(tf.keras.utils.Sequence):

    def __init__(self, files_list, target_size, batch_size,
                 preprocess_images_function=None,
                 preprocess_keypts_function=None,
                 shuffle=False):
        """Custom data generator to use with model.fit_generator()
        
        Params:
            file_list - list with file paths
            target_size - target image size as integer (output img is square)
            batch_size - mini-batch size
            shuffle - shuffle features/targets between epochs
            preprocess_images_function - 
                function to be called on images mini batch,
                e.g. tf.keras.application.resnet50.preprocess_input
            preprocess_keypts_function - same as above but for keypoints
        
        Note:
            this class 'shuffle' param will shuffle all examples between epochs
            model.fit_generator(shuffle=...) param shufles order of mini-batches,
                but does not touch what is inside mini-batch
        """
        assert isinstance(files_list, (list, tuple, np.ndarray))
        assert isinstance(target_size, int) and target_size > 0
        assert isinstance(batch_size, int) and batch_size > 0
        assert preprocess_images_function is None or callable(preprocess_images_function)
        assert preprocess_keypts_function is None or callable(preprocess_keypts_function)
        assert isinstance(shuffle, bool)
        
        self.files_list = np.array(files_list)  # for advanced indexing
        self.target_size = target_size
        self.batch_size = batch_size
        self.preprocess_images_function = preprocess_images_function
        self.preprocess_keypts_function = preprocess_keypts_function
        self.shuffle = shuffle
        self.on_epoch_end()
        

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

    def __getitem__(self, idx):
        batch_i = self.indices[idx*self.batch_size : (idx+1)*self.batch_size]
        batch_fl = self.files_list[batch_i]
        
        images_list, keypoints_list = [], []
        for file_path in batch_fl:
            _, _, image_new, keypoints_new = load_image_keypoints(
                file_path, file_path+'.cat', target_size=self.target_size)
            image_arr = np.array(image_new)
            images_list.append(image_arr)
            keypoints_list.append(keypoints_new)
        
        images_arr = np.array(images_list)
        keypoints_arr = np.array(keypoints_list)
        
        if self.preprocess_images_function is not None:
            images_arr = self.preprocess_images_function(images_arr)
        if self.preprocess_keypts_function is not None:
            keypoints_arr = self.preprocess_keypts_function(keypoints_arr)
        
        return images_arr, keypoints_arr

    def on_epoch_end(self):
        self.indices = np.arange(len(self.files_list))
        if self.shuffle:
            np.random.shuffle(self.indices)

Split into train and validation sets

In [19]:
split = 8000
train_files = image_paths_all[:split]
valid_files = image_paths_all[split:]

Create generators

  • images are normalised in the same way as dataset used for network pretraining (ImageNet)
  • keypoints are normalised to range roughly -1..1
In [20]:
train_generator = CatSequence(train_files, batch_size=32, target_size=224,
    preprocess_images_function=tf.keras.applications.mobilenet_v2.preprocess_input,
    preprocess_keypts_function=lambda keypoints : (keypoints-112) / 112)
valid_generator = CatSequence(valid_files, batch_size=32, target_size=224,
    preprocess_images_function=tf.keras.applications.mobilenet_v2.preprocess_input,
    preprocess_keypts_function=lambda keypoints : (keypoints-112) / 112)

Functions to reverse normalisation, so we can plot images

Note: _undo_preprocessimages is not exact reverse of image normalisation, hence images colour space looks a bit strange. This is ok for our purposes.

In [21]:
def undo_preprocess_images(images_batch):
    tmp = images_batch
    tmp = (tmp + tmp.min()) / (tmp.max() - tmp.min())
    tmp = (tmp * 255).astype(np.uint8)
    return tmp
In [22]:
def undo_preprocess_keypts(keypoints_batch, img_size):
    return (keypoints_batch * (img_size // 2)) + (img_size // 2) 

Show couple samples

In [23]:
train_images, train_keypoints = train_generator[0]
plot_images([5, 10, 15, 20, 25, 30],
            undo_preprocess_images(train_images),
            undo_preprocess_keypts(train_keypoints, img_size=224))

valid_images, valid_keypoints = valid_generator[0]
plot_images([5, 10, 15, 20, 25, 30],
            undo_preprocess_images(valid_images),
            undo_preprocess_keypts(valid_keypoints, img_size=224))

Train End-to-End

Define model

In [24]:
X_inputs = tf.keras.layers.Input(shape=(224, 224, 3))

mobilenetv2 = tf.keras.applications.mobilenet_v2.MobileNetV2(
    input_shape=(224, 224, 3), alpha=1.0, include_top=False,
    weights='imagenet', input_tensor=X_inputs, pooling='max')

X = tf.keras.layers.Dense(128, activation='relu')(mobilenetv2.layers[-1].output)
X = tf.keras.layers.Dense(64, activation='relu')(X)
X = tf.keras.layers.Dense(18, activation='linear')(X)
X = tf.keras.layers.Reshape((9, 2))(X)

model = tf.keras.models.Model(inputs=X_inputs, outputs=X)
model.compile(optimizer=tf.keras.optimizers.Adam(), loss='mse')
WARNING:tensorflow:From /home/marcin/.anaconda/envs/tfgpu113/lib/python3.7/site-packages/tensorflow/python/ops/resource_variable_ops.py:435: colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Colocations handled automatically by placer.
WARNING:tensorflow:From /home/marcin/.anaconda/envs/tfgpu113/lib/python3.7/site-packages/tensorflow/python/keras/utils/losses_utils.py:170: to_float (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.

Custom callback for plotting

In [29]:
class CallbackPlot(tf.keras.callbacks.Callback):
    def __init__(self, train_images, valid_images):
        self.train_images = train_images
        self.valid_images = valid_images
    
    def on_train_begin(self, logs={}):
        pass

    def on_epoch_end(self, batch, logs={}):
        _, iw, ih, _ = self.train_images.shape
        
        predictions = model.predict(self.train_images)
        plot_images([5, 10, 15, 20, 25, 30],
                    undo_preprocess_images(self.train_images),
                    undo_preprocess_keypts(predictions, iw))
        
        predictions = model.predict(self.valid_images)
        plot_images([5, 10, 15, 20, 25, 30],
                    undo_preprocess_images(self.valid_images),
                    undo_preprocess_keypts(predictions, iw))

Show some cats before training. Most probably there won't be any keypoints shown

In [30]:
callback_plt = CallbackPlot(train_images, valid_images)
callback_plt.on_epoch_end(None)
In [31]:
#
#   Callbacks
#

# tb_logdir = os.path.expanduser('~/logs/')
# tb_counter  = len([log for log in os.listdir(tb_logdir) if 'cats' in log]) + 1
# callback_tb = tf.keras.callbacks.TensorBoard(
#     log_dir=tb_logdir + 'cats' + '_' + str(tb_counter), )

callback_mc = tf.keras.callbacks.ModelCheckpoint(
    'model.h5', save_best_only=True, verbose=1)

callback_lr = tf.keras.callbacks.ReduceLROnPlateau(
    monitor='val_loss', factor=0.2, patience=5, verbose=1)

callback_plt = CallbackPlot(train_images, valid_images)

#
#   Train
#
hist = model.fit_generator(train_generator, epochs=50, shuffle=True,
  validation_data=valid_generator,
  callbacks=[
             #callback_tb,     # uncomment to log into TensorBoard
             callback_mc,
             callback_lr,
             #callback_plt,    # uncomment this to enable plotting during training
            ]
)
WARNING:tensorflow:From /home/marcin/.anaconda/envs/tfgpu113/lib/python3.7/site-packages/tensorflow/python/ops/math_ops.py:3066: to_int32 (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
WARNING:tensorflow:From /home/marcin/.anaconda/envs/tfgpu113/lib/python3.7/site-packages/tensorflow/python/ops/math_grad.py:102: div (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Deprecated in favor of operator or tf.math.divide.
Epoch 1/50
63/63 [==============================] - 29s 457ms/step - loss: 2.0777

Epoch 00001: val_loss improved from inf to 2.07768, saving model to model.h5
250/250 [==============================] - 180s 721ms/step - loss: 0.2117 - val_loss: 2.0777
Epoch 2/50
63/63 [==============================] - 25s 390ms/step - loss: 0.7707

Epoch 00002: val_loss improved from 2.07768 to 0.77072, saving model to model.h5
250/250 [==============================] - 138s 553ms/step - loss: 0.0450 - val_loss: 0.7707
Epoch 3/50
63/63 [==============================] - 24s 386ms/step - loss: 0.2596

Epoch 00003: val_loss improved from 0.77072 to 0.25960, saving model to model.h5
250/250 [==============================] - 135s 539ms/step - loss: 0.0238 - val_loss: 0.2596
Epoch 4/50
63/63 [==============================] - 24s 388ms/step - loss: 0.0995

Epoch 00004: val_loss improved from 0.25960 to 0.09947, saving model to model.h5
250/250 [==============================] - 135s 540ms/step - loss: 0.0149 - val_loss: 0.0995
Epoch 5/50
63/63 [==============================] - 24s 383ms/step - loss: 0.0535

Epoch 00005: val_loss improved from 0.09947 to 0.05354, saving model to model.h5
250/250 [==============================] - 136s 543ms/step - loss: 0.0102 - val_loss: 0.0535
Epoch 6/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0426

Epoch 00006: val_loss improved from 0.05354 to 0.04255, saving model to model.h5
250/250 [==============================] - 136s 542ms/step - loss: 0.0077 - val_loss: 0.0426
Epoch 7/50
63/63 [==============================] - 24s 383ms/step - loss: 0.0435

Epoch 00007: val_loss did not improve from 0.04255
250/250 [==============================] - 134s 538ms/step - loss: 0.0060 - val_loss: 0.0435
Epoch 8/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0294

Epoch 00008: val_loss improved from 0.04255 to 0.02938, saving model to model.h5
250/250 [==============================] - 134s 537ms/step - loss: 0.0054 - val_loss: 0.0294
Epoch 9/50
63/63 [==============================] - 25s 389ms/step - loss: 0.0230

Epoch 00009: val_loss improved from 0.02938 to 0.02300, saving model to model.h5
250/250 [==============================] - 134s 536ms/step - loss: 0.0048 - val_loss: 0.0230
Epoch 10/50
63/63 [==============================] - 24s 385ms/step - loss: 0.0211

Epoch 00010: val_loss improved from 0.02300 to 0.02107, saving model to model.h5
250/250 [==============================] - 135s 541ms/step - loss: 0.0041 - val_loss: 0.0211
Epoch 11/50
63/63 [==============================] - 24s 383ms/step - loss: 0.0198

Epoch 00011: val_loss improved from 0.02107 to 0.01982, saving model to model.h5
250/250 [==============================] - 134s 537ms/step - loss: 0.0042 - val_loss: 0.0198
Epoch 12/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0214

Epoch 00012: val_loss did not improve from 0.01982
250/250 [==============================] - 133s 533ms/step - loss: 0.0057 - val_loss: 0.0214
Epoch 13/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0229

Epoch 00013: val_loss did not improve from 0.01982
250/250 [==============================] - 134s 537ms/step - loss: 0.0053 - val_loss: 0.0229
Epoch 14/50
63/63 [==============================] - 25s 390ms/step - loss: 0.0255

Epoch 00014: val_loss did not improve from 0.01982
250/250 [==============================] - 135s 539ms/step - loss: 0.0042 - val_loss: 0.0255
Epoch 15/50
63/63 [==============================] - 24s 385ms/step - loss: 0.0328

Epoch 00015: val_loss did not improve from 0.01982
250/250 [==============================] - 134s 536ms/step - loss: 0.0061 - val_loss: 0.0328
Epoch 16/50
63/63 [==============================] - 25s 390ms/step - loss: 0.0180

Epoch 00016: val_loss improved from 0.01982 to 0.01803, saving model to model.h5
250/250 [==============================] - 136s 543ms/step - loss: 0.0054 - val_loss: 0.0180
Epoch 17/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0233

Epoch 00017: val_loss did not improve from 0.01803
250/250 [==============================] - 133s 534ms/step - loss: 0.0036 - val_loss: 0.0233
Epoch 18/50
63/63 [==============================] - 25s 391ms/step - loss: 0.0113

Epoch 00018: val_loss improved from 0.01803 to 0.01131, saving model to model.h5
250/250 [==============================] - 135s 542ms/step - loss: 0.0028 - val_loss: 0.0113
Epoch 19/50
63/63 [==============================] - 24s 384ms/step - loss: 0.0122

Epoch 00019: val_loss did not improve from 0.01131
250/250 [==============================] - 134s 537ms/step - loss: 0.0024 - val_loss: 0.0122
Epoch 20/50
63/63 [==============================] - 25s 389ms/step - loss: 0.0117

Epoch 00020: val_loss did not improve from 0.01131
250/250 [==============================] - 134s 535ms/step - loss: 0.0021 - val_loss: 0.0117
Epoch 21/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0095

Epoch 00021: val_loss improved from 0.01131 to 0.00955, saving model to model.h5
250/250 [==============================] - 134s 536ms/step - loss: 0.0020 - val_loss: 0.0095
Epoch 22/50
63/63 [==============================] - 24s 388ms/step - loss: 0.0084

Epoch 00022: val_loss improved from 0.00955 to 0.00841, saving model to model.h5
250/250 [==============================] - 134s 536ms/step - loss: 0.0020 - val_loss: 0.0084
Epoch 23/50
63/63 [==============================] - 24s 388ms/step - loss: 0.0440

Epoch 00023: val_loss did not improve from 0.00841
250/250 [==============================] - 134s 535ms/step - loss: 0.0059 - val_loss: 0.0440
Epoch 24/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0517

Epoch 00024: val_loss did not improve from 0.00841
250/250 [==============================] - 135s 540ms/step - loss: 0.0088 - val_loss: 0.0517
Epoch 25/50
63/63 [==============================] - 24s 385ms/step - loss: 0.1297

Epoch 00025: val_loss did not improve from 0.00841
250/250 [==============================] - 134s 537ms/step - loss: 0.0108 - val_loss: 0.1297
Epoch 26/50
63/63 [==============================] - 25s 391ms/step - loss: 0.1254

Epoch 00026: val_loss did not improve from 0.00841
250/250 [==============================] - 135s 539ms/step - loss: 0.0115 - val_loss: 0.1254
Epoch 27/50
63/63 [==============================] - 25s 390ms/step - loss: 0.0668

Epoch 00027: val_loss did not improve from 0.00841

Epoch 00027: ReduceLROnPlateau reducing learning rate to 0.00020000000949949026.
250/250 [==============================] - 136s 542ms/step - loss: 0.0156 - val_loss: 0.0668
Epoch 28/50
63/63 [==============================] - 24s 385ms/step - loss: 0.0636

Epoch 00028: val_loss did not improve from 0.00841
250/250 [==============================] - 134s 535ms/step - loss: 0.0053 - val_loss: 0.0636
Epoch 29/50
63/63 [==============================] - 25s 392ms/step - loss: 0.0394

Epoch 00029: val_loss did not improve from 0.00841
250/250 [==============================] - 135s 540ms/step - loss: 0.0040 - val_loss: 0.0394
Epoch 30/50
63/63 [==============================] - 25s 393ms/step - loss: 0.0225

Epoch 00030: val_loss did not improve from 0.00841
250/250 [==============================] - 136s 542ms/step - loss: 0.0035 - val_loss: 0.0225
Epoch 31/50
63/63 [==============================] - 24s 384ms/step - loss: 0.0135

Epoch 00031: val_loss did not improve from 0.00841
250/250 [==============================] - 135s 540ms/step - loss: 0.0031 - val_loss: 0.0135
Epoch 32/50
63/63 [==============================] - 24s 384ms/step - loss: 0.0115

Epoch 00032: val_loss did not improve from 0.00841

Epoch 00032: ReduceLROnPlateau reducing learning rate to 4.0000001899898055e-05.
250/250 [==============================] - 134s 536ms/step - loss: 0.0028 - val_loss: 0.0115
Epoch 33/50
63/63 [==============================] - 24s 386ms/step - loss: 0.0093

Epoch 00033: val_loss did not improve from 0.00841
250/250 [==============================] - 135s 539ms/step - loss: 0.0025 - val_loss: 0.0093
Epoch 34/50
63/63 [==============================] - 24s 385ms/step - loss: 0.0084

Epoch 00034: val_loss improved from 0.00841 to 0.00836, saving model to model.h5
250/250 [==============================] - 135s 541ms/step - loss: 0.0024 - val_loss: 0.0084
Epoch 35/50
63/63 [==============================] - 24s 385ms/step - loss: 0.0076

Epoch 00035: val_loss improved from 0.00836 to 0.00755, saving model to model.h5
250/250 [==============================] - 134s 537ms/step - loss: 0.0023 - val_loss: 0.0076
Epoch 36/50
63/63 [==============================] - 25s 393ms/step - loss: 0.0072

Epoch 00036: val_loss improved from 0.00755 to 0.00717, saving model to model.h5
250/250 [==============================] - 134s 537ms/step - loss: 0.0022 - val_loss: 0.0072
Epoch 37/50
63/63 [==============================] - 24s 388ms/step - loss: 0.0069

Epoch 00037: val_loss improved from 0.00717 to 0.00695, saving model to model.h5
250/250 [==============================] - 134s 538ms/step - loss: 0.0022 - val_loss: 0.0069
Epoch 38/50
63/63 [==============================] - 24s 383ms/step - loss: 0.0068

Epoch 00038: val_loss improved from 0.00695 to 0.00684, saving model to model.h5
250/250 [==============================] - 135s 539ms/step - loss: 0.0021 - val_loss: 0.0068
Epoch 39/50
63/63 [==============================] - 24s 384ms/step - loss: 0.0067

Epoch 00039: val_loss improved from 0.00684 to 0.00674, saving model to model.h5
250/250 [==============================] - 133s 533ms/step - loss: 0.0021 - val_loss: 0.0067
Epoch 40/50
63/63 [==============================] - 24s 381ms/step - loss: 0.0067

Epoch 00040: val_loss improved from 0.00674 to 0.00668, saving model to model.h5
250/250 [==============================] - 135s 540ms/step - loss: 0.0020 - val_loss: 0.0067
Epoch 41/50
63/63 [==============================] - 24s 382ms/step - loss: 0.0066

Epoch 00041: val_loss improved from 0.00668 to 0.00662, saving model to model.h5
250/250 [==============================] - 134s 537ms/step - loss: 0.0020 - val_loss: 0.0066
Epoch 42/50
63/63 [==============================] - 24s 383ms/step - loss: 0.0066

Epoch 00042: val_loss did not improve from 0.00662
250/250 [==============================] - 134s 537ms/step - loss: 0.0019 - val_loss: 0.0066
Epoch 43/50
63/63 [==============================] - 24s 384ms/step - loss: 0.0066

Epoch 00043: val_loss improved from 0.00662 to 0.00659, saving model to model.h5
250/250 [==============================] - 136s 543ms/step - loss: 0.0019 - val_loss: 0.0066
Epoch 44/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0066

Epoch 00044: val_loss did not improve from 0.00659
250/250 [==============================] - 135s 539ms/step - loss: 0.0018 - val_loss: 0.0066
Epoch 45/50
63/63 [==============================] - 25s 392ms/step - loss: 0.0065

Epoch 00045: val_loss improved from 0.00659 to 0.00654, saving model to model.h5
250/250 [==============================] - 135s 538ms/step - loss: 0.0018 - val_loss: 0.0065
Epoch 46/50
63/63 [==============================] - 24s 388ms/step - loss: 0.0065

Epoch 00046: val_loss improved from 0.00654 to 0.00652, saving model to model.h5

Epoch 00046: ReduceLROnPlateau reducing learning rate to 8.000000525498762e-06.
250/250 [==============================] - 136s 543ms/step - loss: 0.0017 - val_loss: 0.0065
Epoch 47/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0065

Epoch 00047: val_loss did not improve from 0.00652
250/250 [==============================] - 134s 538ms/step - loss: 0.0016 - val_loss: 0.0065
Epoch 48/50
63/63 [==============================] - 25s 391ms/step - loss: 0.0065

Epoch 00048: val_loss improved from 0.00652 to 0.00649, saving model to model.h5
250/250 [==============================] - 136s 543ms/step - loss: 0.0016 - val_loss: 0.0065
Epoch 49/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0065

Epoch 00049: val_loss improved from 0.00649 to 0.00648, saving model to model.h5
250/250 [==============================] - 135s 540ms/step - loss: 0.0016 - val_loss: 0.0065
Epoch 50/50
63/63 [==============================] - 24s 387ms/step - loss: 0.0065

Epoch 00050: val_loss improved from 0.00648 to 0.00647, saving model to model.h5
250/250 [==============================] - 134s 536ms/step - loss: 0.0016 - val_loss: 0.0065

The final train loss should be around 0.0020 and final validation loss around 0.0060

Plot loss during training

In [32]:
_, (ax1, ax2, ax3) = plt.subplots(nrows=1, ncols=3, figsize=(12,4))
ax1.plot(hist.history['loss'], label='loss')
ax1.plot(hist.history['val_loss'], label='val_loss')
ax1.legend()

ax2.plot(hist.history['loss'], label='loss')
ax2.plot(hist.history['val_loss'], label='val_loss')
ax2.legend()
ax2.set_ylim(0, .1)

ax3.plot(hist.history['lr'], label='lr')
ax3.legend()

plt.tight_layout()
plt.show()

Show some cats from validation set - looks pretty good

In [39]:
for i in range(10):
    valid_images, _ = valid_generator[i]
    predictions = model.predict(valid_images)
    plot_images([5, 10, 15, 20, 25, 30],
                undo_preprocess_images(valid_images),
                undo_preprocess_keypts(predictions, img_size=224))