4

My X_test are 128x128x3 images and my Y_test are 512x512x3 images. I want to show, after each epoch, how the input (X_test) looked, how the expected output (Y_test) looked, but also how the actual output looked. So far, I've only figured out how to add the first 2 in Tensorboard. Here is the code that calls the Callback:

model.fit(X_train,
          Y_train,
          epochs=epochs,
          verbose=2,
          shuffle=False,
          validation_data=(X_test, Y_test),
          batch_size=batch_size,
          callbacks=get_callbacks())

Here is the Callback's code:

import tensorflow as tf
from keras.callbacks import Callback
from keras.callbacks import TensorBoard

import io
from PIL import Image

from constants import batch_size


def get_callbacks():
    tbCallBack = TensorBoard(log_dir='./logs',
                             histogram_freq=1,
                             write_graph=True,
                             write_images=True,
                             write_grads=True,
                             batch_size=batch_size)

    tbi_callback = TensorBoardImage('Image test')

    return [tbCallBack, tbi_callback]


def make_image(tensor):
    """
    Convert an numpy representation image to Image protobuf.
    Copied from https://github.com/lanpa/tensorboard-pytorch/
    """
    height, width, channel = tensor.shape
    print(tensor)
    image = Image.fromarray(tensor.astype('uint8'))  # TODO: maybe float ?

    output = io.BytesIO()
    image.save(output, format='JPEG')
    image_string = output.getvalue()
    output.close()

    return tf.Summary.Image(height=height,
                            width=width,
                            colorspace=channel,
                            encoded_image_string=image_string)


class TensorBoardImage(Callback):
    def __init__(self, tag):
        super().__init__()
        self.tag = tag

    def on_epoch_end(self, epoch, logs={}):
        # Load image
        img_input = self.validation_data[0][0]  # X_train
        img_valid = self.validation_data[1][0]  # Y_train

        print(self.validation_data[0].shape)  # (8, 128, 128, 3)
        print(self.validation_data[1].shape)  # (8, 512, 512, 3)

        image = make_image(img_input)
        summary = tf.Summary(value=[tf.Summary.Value(tag=self.tag, image=image)])
        writer = tf.summary.FileWriter('./logs')
        writer.add_summary(summary, epoch)
        writer.close()

        image = make_image(img_valid)
        summary = tf.Summary(value=[tf.Summary.Value(tag=self.tag, image=image)])
        writer = tf.summary.FileWriter('./logs')
        writer.add_summary(summary, epoch)
        writer.close()

        return

I'm wondering where/how I can get the actual output of the network.

Another issue I'm having is that here is a sample of one of the images that is being ported into TensorBoard:

[[[0.10909907 0.09341043 0.08224604]
  [0.11599099 0.09922747 0.09138277]
  [0.15596421 0.13087936 0.11472746]
  ...
  [0.87589591 0.72773653 0.69428956]
  [0.87006552 0.7218123  0.68836991]
  [0.87054225 0.72794635 0.6967475 ]]

 ...

 [[0.26142332 0.16216267 0.10314116]
  [0.31526875 0.18743924 0.12351286]
  [0.5499796  0.35461449 0.24772873]
  ...
  [0.80937942 0.62956016 0.53784871]
  [0.80906054 0.62843601 0.5368183 ]
  [0.81046278 0.62453899 0.53849678]]]

Is that the reason why my image = Image.fromarray(tensor.astype('uint8')) line might be generating images that do not look at all like the actual output? Here is a sample from TensorBoard:

Output images

I did try .astype('float64') but it launched an error because it is apparently not a type that is supported.

Anyhow, I'm unsure this really is the problem since the rest of my displayed images in the TensorBoard are all just white/gray/black squares (this one right there, conv2D_7, is actually the very last layer of my network and should thus display the actual images that are outputted, no?):

TensorBoard convs

Ultimately, I would like something like this, which I'm already displaying after the training through matplot:

Results

Finally, I would like to adress the fact that this callback is taking a long time to process. Is there a more efficient way to do that? It almost doubles my training time (probably because it needs to convert the numpy into images before saving them in the TensorBoard Log file).

rpanai
  • 12,515
  • 2
  • 42
  • 64
payne
  • 4,691
  • 8
  • 37
  • 85
  • I am running into the same issue, were you able to solve this? – Josiah L. Nov 03 '18 at 23:54
  • Unfortunately no, I just decided to remove the callback because it was taking too long to process anyways. My latest guess would be that Tensorboard expects a range from 0 to 256, but that I'm sending 0 to 1. – payne Nov 04 '18 at 00:00
  • Dear, the images you have showed are weights displayed as Image(write_images=True saves weights as image, google beholder and watch its video). Not the output of convolution. Next, your callback should have handle to the model and you need to do forward pass on a subset(1,2 batches should give you enough images to ) of validation images to get the result. Next, images that you get need to be unnormalized(add mean, multiply by stddev, etc) and then scaled to 0, 255 – saurabheights Mar 13 '19 at 12:49
  • @saurabheights is there some boiler plate code or template that I could use to save some time? (I'm currently in an end-of-trimester rush and won't be able to touch this for a while.) – payne Mar 13 '19 at 15:45
  • 1
    @payne Not yet, but I will book mark your question and post it once done. – saurabheights Mar 13 '19 at 15:55
  • @payne: Done, but I am similar situation as u, thesis time, last semester. so code cleanup from my repository may not be perfect. – saurabheights Apr 25 '19 at 19:55
  • 1
    @saurabheights MVP right there. I -just- finished my Bachelor today, but we still have two assigments to work on. I'll look into your solution within a week, I would guess. Thank you so much! – payne Apr 26 '19 at 06:09

2 Answers2

4

The below code takes input to model, output of model and ground truth and saves to Tensorboard. The model is segmentation, thus 3 images per sample.

The code is quite simple and straightforward. But still a few explanation:-

make_image_tensor - The method converts the numpy image and creates a tensor to save in tensorboard summary.

TensorBoardWriter - Not required, but its good to keep Tensorboard functionality separate from other modules. Allows Reusability.

ModelDiagonoser - The class which takes a generator, and predicts over self.model(set by Keras to all callbacks). The ModelDiagonoser takes input, output and groundtruth and passes to Tensorboard to save the images.

import os

import io
import numpy as np
import tensorflow as tf
from PIL import Image
from tensorflow.keras.callbacks import Callback

# Depending on your keras version use one of the following:
# from tensorflow.keras.engine.training import GeneratorEnqueuer, Sequence, OrderedEnqueuer
from tensorflow.keras.utils import GeneratorEnqueuer, Sequence, OrderedEnqueuer


def make_image_tensor(tensor):
    """
    Convert an numpy representation image to Image protobuf.
    Adapted from https://github.com/lanpa/tensorboard-pytorch/
    """
    if len(tensor.shape) == 3:
        height, width, channel = tensor.shape
    else:
        height, width = tensor.shape
        channel = 1
    tensor = tensor.astype(np.uint8)
    image = Image.fromarray(tensor)
    output = io.BytesIO()
    image.save(output, format='PNG')
    image_string = output.getvalue()
    output.close()
    return tf.Summary.Image(height=height,
                            width=width,
                            colorspace=channel,
                            encoded_image_string=image_string)


class TensorBoardWriter:

    def __init__(self, outdir):
        assert (os.path.isdir(outdir))
        self.outdir = outdir
        self.writer = tf.summary.FileWriter(self.outdir,
                                            flush_secs=10)

    def save_image(self, tag, image, global_step=None):
        image_tensor = make_image_tensor(image)
        self.writer.add_summary(tf.Summary(value=[tf.Summary.Value(tag=tag, image=image_tensor)]),
                                global_step)

    def close(self):
        """
        To be called in the end
        """
        self.writer.close()


class ModelDiagonoser(Callback):

    def __init__(self,
                 data_generator,
                 batch_size,
                 num_samples,
                 output_dir,
                 normalization_mean):
        self.batch_size = batch_size
        self.num_samples = num_samples
        self.tensorboard_writer = TensorBoardWriter(output_dir)
        self.normalization_mean = normalization_mean
        is_sequence = isinstance(data_generator, Sequence)
        if is_sequence:
            self.enqueuer = OrderedEnqueuer(data_generator,
                                            use_multiprocessing=True,
                                            shuffle=False)
        else:
            self.enqueuer = GeneratorEnqueuer(data_generator,
                                              use_multiprocessing=True)
        self.enqueuer.start(workers=4, max_queue_size=4)

    def on_epoch_end(self, epoch, logs=None):
        output_generator = self.enqueuer.get()
        steps_done = 0
        total_steps = int(np.ceil(np.divide(self.num_samples, self.batch_size)))
        sample_index = 0
        while steps_done < total_steps:
            generator_output = next(output_generator)
            x, y = generator_output[:2]
            y_pred = self.model.predict(x)
            y_pred = np.argmax(y_pred, axis=-1)
            y_true = np.argmax(y, axis=-1)

            for i in range(0, len(y_pred)):
                n = steps_done * self.batch_size + i
                if n >= self.num_samples:
                    return
                img = np.squeeze(x[i, :, :, :])
                img = 255. * (img + self.normalization_mean)  # mean is the training images normalization mean
                img = img[:, :, [2, 1, 0]]  # reordering of channels

                pred = y_pred[i]
                pred = pred.reshape(img.shape[0:2])

                ground_truth = y_true[i]
                ground_truth = ground_truth.reshape(img.shape[0:2])

                self.tensorboard_writer.save_image("Epoch-{}/{}/x"
                                                   .format(epoch, sample_index), img)
                self.tensorboard_writer.save_image("Epoch-{}/{}/y"
                                                   .format(epoch, sample_index), ground_truth)
                self.tensorboard_writer.save_image("Epoch-{}/{}/y_pred"
                                                   .format(epoch, sample_index), pred)
                sample_index += 1

            steps_done += 1

    def on_train_end(self, logs=None):
        self.enqueuer.stop()
        self.tensorboard_writer.close()
hafiz031
  • 2,236
  • 3
  • 26
  • 48
saurabheights
  • 3,967
  • 2
  • 31
  • 50
  • P.S. - Dont save thousand samples, just a few is enough to diagnose the model. – saurabheights Apr 26 '19 at 09:51
  • You have 3 undefined variables in there: `workers`, `mean`, and `progbar`. And what is the `data_generator` parameter for? (it is unused) – payne May 05 '19 at 16:13
  • 1
    @payne: Hi, workers is the number of processes/threads you want to use for generating data of next batch. Mean is the normalization mean you perform when training. Its R, G, B value. progbar is ProgressBar utility. Its a common library to generate a progress bar on command line: https://pypi.org/project/progressbar2/ – saurabheights May 05 '19 at 19:46
  • Set workers to 1 for now. Mean is something you will have to pass to constructor. Sorry, I did clean the original code, but missed these two. – saurabheights May 05 '19 at 19:48
  • I have made few minor edits. Lemme know how it goes. P.S. The code is tested, so there should be no major issues in making it work in a different pipeline. – saurabheights May 06 '19 at 12:18
  • Thanks. The `data_generator` refers to [something like this](https://keras.io/preprocessing/image/)? – payne May 07 '19 at 06:14
  • @payne: Yes, but in general, It needs to be a python generator, more info here:- https://github.com/keras-team/keras/wiki/Understanding-parallelism-in-Keras. But overall try pluging a working generator you have into this callback. Overall, the above code is just one example of how to do prediction in callback over some validation/training data. The exact implementation(which type of generator, predict/predict_generator) can be changed according to your needs. – saurabheights May 07 '19 at 11:40
  • Also, in most SO answer, people just remove the code and comment "Do prediction here", but I avoid that as editing code is much faster than writing from scratch. At least this code still gives some idea of what needs to be done for performing prediction, however it may not fit perfectly, since your code will also depend slightly on task in hand - classification, segmentation, etc – saurabheights May 07 '19 at 11:43
  • 1
    Bear in mind: this is my first Python project, first real ML project and first interaction with a ML framework (namely, `Keras`). Back when I first started, a while ago, I was simply using `fit`, splitting my data in folders and manually changing which piece of data I wanted to train on. The automatization of this task was left in a `todo` which I've now just picked up (I [was having problems](https://stackoverflow.com/q/52462058/9768291) with that). Just today I started the transition into using `fit_generator`. [My repo](https://github.com/payne911/SR-ResCNN-Keras-), if you're curious. – payne May 07 '19 at 14:58
  • Cool project. Super-resolution is really exciting work :). – saurabheights May 07 '19 at 21:20
  • I'm not sure how to pop the "Continue in chat" link, but would you be interested in chatting for a little while if you feel like helping me figuring out certain details about my project? – payne May 08 '19 at 12:27
2

Probably the img_input and img_valid are in range of 0 to 1. Converting them to uint8 type will solve the problem.

img_input = self.validation_data[0][0] 
# img_input = img_input / np.max(img_input) # if img_input is not in (0,1), rescale it.
img_input = (255*img_input).astype(np.uint8)
img_valid = self.validation_data[1][0]  # Y_train
img_valid = (255*img_valid ).astype(np.uint8)
翟志伟
  • 23
  • 5