Tensorflow - adding Dropout layer increases inference time significantly - performance

I have relatively small CNN
model = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(input_shape=(400,400,3), filters=6, kernel_size=5, padding='same', activation='relu'),
tf.keras.layers.Conv2D(filters=12, kernel_size=3, padding='same', activation='relu'),
tf.keras.layers.Conv2D(filters=24, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Conv2D(filters=32, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Conv2D(filters=48, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Conv2D(filters=64, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Conv2D(filters=96, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Conv2D(filters=128, kernel_size=3, strides=2, padding='valid', activation='relu'),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dense(240, activation='softmax')
])
model.compile(optimizer='adam', loss='categorical_crossentropy')
I use the following code to measure model performance:
for img_per_batch in [1, 5, 10, 50]:
# warm up the model
image = np.random.random(size=(img_per_batch, 400, 400, 3)).astype('float32')
model(image, training=False)
n_iter = 100
start_time = time.time()
for _ in range(n_iter):
image = np.random.random(size=(img_per_batch, 400, 400, 3)).astype('float32')
model(image, training=False)
dt = (time.time() - start_time) * 1000
print(f'img_per_batch = {img_per_batch}, {dt/n_iter:.2f} ms per iteration, {dt/n_iter/img_per_batch:.2f} ms per image')
My output (Nvidia Jetson Xavier, tensorflow==2.0.0):
img_per_batch = 1, 21.74 ms per iteration, 21.74 ms per image
img_per_batch = 5, 42.35 ms per iteration, 8.47 ms per image
img_per_batch = 10, 68.37 ms per iteration, 6.84 ms per image
img_per_batch = 50, 312.83 ms per iteration, 6.26 ms per image
Then I add dropout layer after each of the fully connected layers:
model = tf.keras.models.Sequential([
# ... convolution layers are same
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(.3),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(.3),
tf.keras.layers.Dense(256, activation='relu'),
tf.keras.layers.Dropout(.3),
tf.keras.layers.Dense(240, activation='softmax')
])
With added layers output becomes as bellow:
img_per_batch = 1, 31.18 ms per iteration, 31.18 ms per image
img_per_batch = 5, 76.15 ms per iteration, 15.23 ms per image
img_per_batch = 10, 127.91 ms per iteration, 12.79 ms per image
img_per_batch = 50, 513.85 ms per iteration, 10.28 ms per image
In theory dropout layer shouldn't impact inference performance. But in the code above adding dropout layer increase single-image prediction time in 1.5 times and 10-images batch prediction is almost twice slower than without dropout. Am I doing something wrong?

Apparently this is a known problem in TensorFlow 2.0.0: see this GitHub comment.
Try to use model.predict(x) instead of model(x).
This can also be fixed by updating to a more recent version of TensorFlow like 2.1.0.
Hope this helps

Related

List of possible reducer functions when using #distributed in Julia

I was reading the documentation for writing parallel for loops in Julia using #distributed and saw that it is possible to specify a reducer function that acts on the result of each iteration of the loop.
For instance, as it is shown in the next example taken from the documentation, it is possible to sum the result of every single worker:
nheads = #distributed (+) for i = 1:200000000
Int(rand(Bool))
end
Unfortunately I couldn't find any list of which functions can be used as reducers and how to exactly do it. Is there such a list?
You can take any function that takes two arguments so the list is open and can be arbitrarily extended. See e.g.
julia> addprocs(4);
julia> x = #distributed (a,b) -> (a,b, "val") for i in 1:10
i
end
(((((1, 2, "val"), 3, "val"), ((4, 5, "val"), 6, "val"), "val"), (7, 8, "val"), "val"), (9, 10, "val"), "val")
julia> addprocs(4);
julia> x = #distributed (a,b) -> (a,b, "val") for i in 1:10
i
end
((((((((1, 2, "val"), (3, 4, "val"), "val"), 5, "val"), 6, "val"), 7, "val"), 8, "val"), 9, "val"), 10, "val")
However, for the operation to work in typical scenarios the function has a signature fun(::T, ::T)::T where T so that it is guaranteed that the reduction operation can be always performed and preferably the result of reduction does not depend on the order of operations (you can see in the example above that the order of reductions depends on the number of workers and I have chosen a function that does not have this property on purpose).

Why does adding dropout layers work on validation set, but not on test set?

I'm working on a convolutional neural network in TensorFlow and having trouble with the dropout layers. As recommended, I'm passing a keep_probability placeholder to the graph and setting the value to 0.5 during training, and 1.0 during validation and testing. When observing the training process, the results are good for the validation set. However, when I test the network after training, the network fails.
UPDATE: When I say that the network fails, I mean that the network no longer segments the images correctly. During validation the network gets an mIoU of around 80%, but when testing it falls down to around 40% and classifies all the pixels into just one of the classes. Before the dropout layers were added, both validation and test set got an mIoU of around 80%.
I do not understand why the network is failing on the test set when it works on the validation set?
I've added the code for training, testing and for the graph itself.
Code for training the network:
with tf.Graph().as_default():
#Probablitity that the neuron's output will be kept during dropout
keep_probability = tf.placeholder(tf.float32, name="keep_probabilty")
global_step = tf.Variable(0, trainable=False)
images, labels = Inputs.datasetInputs(image_filenames, label_filenames, FLAGS.batch_size)
val_images, val_labels = Inputs.datasetInputs(val_image_filenames, val_label_filenames, FLAGS.batch_size)
train_data_node = tf.placeholder(tf.float32, shape=[FLAGS.batch_size, FLAGS.image_h, FLAGS.image_w, 3])
train_labels_node = tf.placeholder(tf.int64, shape=[FLAGS.batch_size, FLAGS.image_h, FLAGS.image_w, 1])
phase_train = tf.placeholder(tf.bool, name='phase_train')
logits = model.inference(train_data_node, phase_train, FLAGS.batch_size, keep_probability) #tensor, nothing calculated yet
loss = model.cal_loss(logits, train_labels_node)
# Build a Graph that trains the model with one batch of examples and updates the model parameters.
train_op = model.train(loss, global_step)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
sess.run(tf.local_variables_initializer())
""" Starting iterations to train the network """
for step in range(startstep, startstep + FLAGS.max_steps):
image_batch ,label_batch = sess.run(fetches=[images, labels])
# since we still use mini-batches in eval, still set bn-layer phase_train = True
feed_dict = {
train_data_node: image_batch,
train_labels_node: label_batch,
phase_train: True,
keep_probability: 0.5
}
_, loss_value = sess.run(fetches=[train_op, loss], feed_dict=feed_dict)
if step % 10 == 0:
num_examples_per_step = FLAGS.batch_size
examples_per_sec = num_examples_per_step / duration
sec_per_batch = float(duration)
# eval current training batch pre-class accuracy
pred = sess.run(fetches=logits, feed_dict=feed_dict)
Utils.per_class_acc(pred, label_batch)
if step % 100 == 0 or (step + 1) == FLAGS.max_steps:
""" Validate training by running validation dataset """
total_val_loss = 0.0
hist = np.zeros((FLAGS.num_class, FLAGS.num_class))
for test_step in range(TEST_ITER):
val_images_batch, val_labels_batch = sess.run(fetches=[val_images, val_labels])
feed_dict = {
train_data_node: val_images_batch,
train_labels_node: val_labels_batch,
phase_train: True,
keep_probability: 1.0 #During testing droput should be turned off -> 100% chance for keeping variable
}
_val_loss, _val_pred = sess.run(fetches=[loss, logits], feed_dict=feed_dict)
(...)
Code for testing the network:
keep_probability = tf.placeholder(tf.float32, name="keep_probabilty")
image_filenames, label_filenames = Inputs.get_filename_list(FLAGS.test_dir)
test_data_node = tf.placeholder(tf.float32, shape=[testing_batch_size, FLAGS.image_h, FLAGS.image_w, FLAGS.image_c]) #360, 480, 3
test_labels_node = tf.placeholder(tf.int64, shape=[FLAGS.test_batch_size, FLAGS.image_h, FLAGS.image_w, 1])
phase_train = tf.placeholder(tf.bool, name='phase_train')
logits = model.inference(test_data_node, phase_train, testing_batch_size, keep_probability)
loss = model.cal_loss(logits, test_labels_node)
pred = tf.argmax(logits, dimension=3)
with tf.Session() as sess:
# Load checkpoint
saver.restore(sess, FLAGS.model_ckpt_dir)
images, labels = Inputs.get_all_test_data(image_filenames, label_filenames)
threads = tf.train.start_queue_runners(sess=sess)
hist = np.zeros((FLAGS.num_class, FLAGS.num_class))
step=0
for image_batch, label_batch in zip(images, labels):
feed_dict = { #maps graph elements to values
test_data_node: image_batch,
test_labels_node: label_batch,
phase_train: False,
keep_probability: 1.0 #During testing droput should be turned off -> 100% chance for keeping variable
}
dense_prediction, im = sess.run(fetches=[logits, pred], feed_dict=feed_dict)
(...)
The graph:
def inference(images, phase_train, batch_size, keep_prob):
conv1_1 = conv_layer_with_bn(images, [7, 7, images.get_shape().as_list()[3], 64], phase_train, name="conv1_1")
conv1_2 = conv_layer_with_bn(conv1_1, [7, 7, 64, 64], phase_train, name="conv1_2")
dropout1 = tf.layers.dropout(conv1_2, rate=(1-keep_prob), training=phase_train, name="dropout1")
pool1, pool1_indices = tf.nn.max_pool_with_argmax(dropout1, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool1')
conv2_1 = conv_layer_with_bn(pool1, [7, 7, 64, 64], phase_train, name="conv2_1")
conv2_2 = conv_layer_with_bn(conv2_1, [7, 7, 64, 64], phase_train, name="conv2_2")
dropout2 = tf.layers.dropout(conv2_2, rate=(1-keep_prob), training=phase_train, name="dropout2")
pool2, pool2_indices = tf.nn.max_pool_with_argmax(dropout2, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool2')
conv3_1 = conv_layer_with_bn(pool2, [7, 7, 64, 64], phase_train, name="conv3_1")
conv3_2 = conv_layer_with_bn(conv3_1, [7, 7, 64, 64], phase_train, name="conv3_2")
conv3_3 = conv_layer_with_bn(conv3_2, [7, 7, 64, 64], phase_train, name="conv3_3")
dropout3 = tf.layers.dropout(conv3_3, rate=(1-keep_prob), training=phase_train, name="dropout3")
pool3, pool3_indices = tf.nn.max_pool_with_argmax(dropout3, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool3')
conv4_1 = conv_layer_with_bn(pool3, [7, 7, 64, 64], phase_train, name="conv4_1")
conv4_2 = conv_layer_with_bn(conv4_1, [7, 7, 64, 64], phase_train, name="conv4_2")
conv4_3 = conv_layer_with_bn(conv4_2, [7, 7, 64, 64], phase_train, name="conv4_3")
dropout4 = tf.layers.dropout(conv4_3, rate=(1-keep_prob), training=phase_train, name="dropout4")
pool4, pool4_indices = tf.nn.max_pool_with_argmax(dropout4, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool4')
conv5_1 = conv_layer_with_bn(pool4, [7, 7, 64, 64], phase_train, name="conv5_1")
conv5_2 = conv_layer_with_bn(conv5_1, [7, 7, 64, 64], phase_train, name="conv5_2")
conv5_3 = conv_layer_with_bn(conv5_2, [7, 7, 64, 64], phase_train, name="conv5_3")
dropout5 = tf.layers.dropout(conv5_3, rate=(1-keep_prob), training=phase_train, name="dropout5")
pool5, pool5_indices = tf.nn.max_pool_with_argmax(dropout5, ksize=[1, 2, 2, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool5')
""" End of encoder """
""" Start decoder """
dropout5_decode = tf.layers.dropout(pool5, rate=(1-keep_prob), training=phase_train, name="dropout5_decode")
upsample5 = deconv_layer(dropout5_decode, [2, 2, 64, 64], [batch_size, FLAGS.image_h//16, FLAGS.image_w//16, 64], 2, "up5")
conv_decode5_1 = conv_layer_with_bn(upsample5, [7, 7, 64, 64], phase_train, True, name="conv_decode5_1")
conv_decode5_2 = conv_layer_with_bn(conv_decode5_1, [7, 7, 64, 64], phase_train, True, name="conv_decode5_2")
conv_decode5_3 = conv_layer_with_bn(conv_decode5_2, [7, 7, 64, 64], phase_train, True, name="conv_decode5_3")
dropout4_decode = tf.layers.dropout(conv_decode5_3, rate=(1-keep_prob), training=phase_train, name="dropout4_decode")
upsample4 = deconv_layer(dropout4_decode, [2, 2, 64, 64], [batch_size, FLAGS.image_h//8, FLAGS.image_w//8, 64], 2, "up4")
conv_decode4_1 = conv_layer_with_bn(upsample4, [7, 7, 64, 64], phase_train, True, name="conv_decode4_1")
conv_decode4_2 = conv_layer_with_bn(conv_decode4_1, [7, 7, 64, 64], phase_train, True, name="conv_decode4_2")
conv_decode4_3 = conv_layer_with_bn(conv_decode4_2, [7, 7, 64, 64], phase_train, True, name="conv_decode4_3")
dropout3_decode = tf.layers.dropout(conv_decode4_3, rate=(1-keep_prob), training=phase_train, name="dropout3_decode")
upsample3 = deconv_layer(dropout3_decode, [2, 2, 64, 64], [batch_size, FLAGS.image_h//4, FLAGS.image_w//4, 64], 2, "up3")
conv_decode3_1 = conv_layer_with_bn(upsample3, [7, 7, 64, 64], phase_train, True, name="conv_decode3_1")
conv_decode3_2 = conv_layer_with_bn(conv_decode3_1, [7, 7, 64, 64], phase_train, True, name="conv_decode3_2")
conv_decode3_3 = conv_layer_with_bn(conv_decode3_2, [7, 7, 64, 64], phase_train, True, name="conv_decode3_3")
dropout2_decode = tf.layers.dropout(conv_decode3_3, rate=(1-keep_prob), training=phase_train, name="dropout2_decode")
upsample2= deconv_layer(dropout2_decode, [2, 2, 64, 64], [batch_size, FLAGS.image_h//2, FLAGS.image_w//2, 64], 2, "up2")
conv_decode2_1 = conv_layer_with_bn(upsample2, [7, 7, 64, 64], phase_train, True, name="conv_decode2_1")
conv_decode2_2 = conv_layer_with_bn(conv_decode2_1, [7, 7, 64, 64], phase_train, True, name="conv_decode2_2")
dropout1_decode = tf.layers.dropout(conv_decode2_2, rate=(1-keep_prob), training=phase_train, name="dropout1_deconv")
upsample1 = deconv_layer(dropout1_decode, [2, 2, 64, 64], [batch_size, FLAGS.image_h, FLAGS.image_w, 64], 2, "up1")
conv_decode1_1 = conv_layer_with_bn(upsample1, [7, 7, 64, 64], phase_train, True, name="conv_decode1_1")
conv_decode1_2 = conv_layer_with_bn(conv_decode1_1, [7, 7, 64, 64], phase_train, True, name="conv_decode1_2")
""" End of decoder """
""" Start Classify """
# output predicted class number (2)
with tf.variable_scope('conv_classifier') as scope:
shape=[1, 1, 64, FLAGS.num_class]
kernel = _variable_with_weight_decay('weights', shape=shape, initializer=tf.contrib.layers.variance_scaling_initializer(), #orthogonal_initializer()
wd=None)
conv = tf.nn.conv2d(conv_decode1_2, kernel, [1, 1, 1, 1], padding='SAME')
biases = _variable_on_cpu('biases', [FLAGS.num_class], tf.constant_initializer(0.0))
conv_classifier = tf.nn.bias_add(conv, biases, name=scope.name) #tf.nn.bias_add is an activation function. Simple add that specifies 1-D tensor bias
#logit = conv_classifier = prediction
return conv_classifier
I eventually figured out that my issue came from problems with batch normalization not being implemented correctly.
The issue was that I did not update the moving_mean and moving_variance correctly. This is pointed out in the TensorFlow docs for batch_norm:
Note: when training, the moving_mean and moving_variance need to be
updated. By default the update ops are placed in
tf.GraphKeys.UPDATE_OPS, so they need to be added as a dependency to
the train_op.
My code, therefore, looks like this:
def training(loss):
global_step = tf.Variable(0, name='global_step', trainable=False)
#This motif is needed to hook up the batch_norm updates to the training
update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS)
with tf.control_dependencies(update_ops):
if(FLAGS.optimizer == "SGD"):
print("Running with SGD optimizer")
optimizer = tf.train.GradientDescentOptimizer(0.1)
elif(FLAGS.optimizer == "adam"):
print("Running with adam optimizer")
optimizer = tf.train.AdamOptimizer(0.001)
elif(FLAGS.optimizer == "adagrad"):
print("Running with adagrad optimizer")
optimizer = tf.train.AdagradOptimizer(0.01)
else:
raise ValueError("optimizer was not recognized.")
train_op = optimizer.minimize(loss=loss, global_step=global_step)
return train_op, global_step
Full implementation of my network can be seen here: https://github.com/mathildor/TF-SegNet

Keras doesn't train using fit_generator()

I am using Keras 2.0.4 (TensorFlow backend) for an image classification task.
I am trying to train my own network (without any pretrained parameters).
As my data is huge I cannot load all into memory.
For this reason I use ImageDataGenerator(), flow_from_directory() and fit_generator().
Creating ImageDataGenerator object:
train_datagen = ImageDataGenerator(preprocessing_function = my_preprocessing_function) # only preprocessing; no augmentation; static data set
my_preprocessing_function rescales images to domain [0,255] and centers data by mean reduction (similar to preprocessing of VGG16 or VGG19)
Use method flow_from_directory() from the ImageDataGenerator object:
train_generator = train_datagen.flow_from_directory(
path/to/training/directory/with/five/subfolders,
target_size=(img_width, img_height),
batch_size=64,
classes = ['class1', 'class2', 'class3', 'class4', 'class5'],
shuffle = True,
seed = 1337,
class_mode='categorical')
(The same is done in order to create a validation_generator.)
After defining and compiling the model (loss function: categorical crossentropy, optimizer: Adam), I train the model using fit_generator():
model.fit_generator(
train_generator,
steps_per_epoch=total_amount_of_train_samples/batch_size,
epochs=400,
validation_data=validation_generator,
validation_steps=total_amount_of_validation_samples/batch_size)
Problem:
There is no error message, but training doesn't perform well.
After 400 epochs, accuracy still oscillates around 20% (which is as good as randomly choosing one of those classes). Indeed, the classifier always predicts 'class1'.
The same holds true after only one epoch of training. Why is this the case although I am initializing random weights?
What is wrong? What am I missing?
U S E D M O D E L
x = Input(shape=input_shape)
# Block 1
x = Conv2D(16, (3, 3), activation='relu', padding='same', name='block1_conv1')(x)
x = Conv2D(16, (5, 5), activation='relu', padding='same', name='block1_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), name='block1_pool')(x)
# Block 2
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
x = Conv2D(64, (5, 5), activation='relu', padding='same', name='block2_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), name='block2_pool')(x)
# Block 3
x = Conv2D(16, (1, 1), activation='relu', padding='same', name='block3_conv1')(x)
# Block 4
x = Conv2D(256, (3, 3), activation='relu', padding='valid', name='block4_conv1')(x)
x = Conv2D(256, (5, 5), activation='relu', padding='valid', name='block4_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), name='block4_pool')(x)
# Block 5
x = Conv2D(1024, (3, 3), activation='relu', padding='valid', name='block5_conv1')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), name='block5_pool')(x)
# topping
x = Dense(1024, activation='relu', name='fc1')(x)
x = Dense(1024, activation='relu', name='fc2')(x)
predictions = Dense(5, activation='softmax', name='predictions')(x)
E D I T
terminal output

How to visualize learned filters on tensorflow

Similarly to the Caffe framework, where it is possible to watch the learned filters during CNNs training and it's resulting convolution with input images, I wonder if is it possible to do the same with TensorFlow?
A Caffe example can be viewed in this link:
http://nbviewer.jupyter.org/github/BVLC/caffe/blob/master/examples/00-classification.ipynb
Grateful for your help!
To see just a few conv1 filters in Tensorboard, you can use this code (it works for cifar10)
# this should be a part of the inference(images) function in cifar10.py file
# conv1
with tf.variable_scope('conv1') as scope:
kernel = _variable_with_weight_decay('weights', shape=[5, 5, 3, 64],
stddev=1e-4, wd=0.0)
conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
bias = tf.nn.bias_add(conv, biases)
conv1 = tf.nn.relu(bias, name=scope.name)
_activation_summary(conv1)
with tf.variable_scope('visualization'):
# scale weights to [0 1], type is still float
x_min = tf.reduce_min(kernel)
x_max = tf.reduce_max(kernel)
kernel_0_to_1 = (kernel - x_min) / (x_max - x_min)
# to tf.image_summary format [batch_size, height, width, channels]
kernel_transposed = tf.transpose (kernel_0_to_1, [3, 0, 1, 2])
# this will display random 3 filters from the 64 in conv1
tf.image_summary('conv1/filters', kernel_transposed, max_images=3)
I also wrote a simple gist to display all 64 conv1 filters in a grid.

fitting n variable height images into 3 (similar length) column layout

I'm looking to make a 3-column layout similar to that of piccsy.com. Given a number of images of the same width but varying height, what is a algorithm to order them so that the difference in column lengths is minimal? Ideally in Python or JavaScript...
Thanks a lot for your help in advance!
Martin
How many images?
If you limit the maximum page size, and have a value for the minimum picture height, you can calculate the maximum number of images per page. You would need this when evaluating any solution.
I think there were 27 pictures on the link you gave.
The following uses the first_fit algorithm mentioned by Robin Green earlier but then improves on this by greedy swapping.
The swapping routine finds the column that is furthest away from the average column height then systematically looks for a swap between one of its pictures and the first picture in another column that minimizes the maximum deviation from the average.
I used a random sample of 30 pictures with heights in the range five to 50 'units'. The convergenge was swift in my case and improved significantly on the first_fit algorithm.
The code (Python 3.2:
def first_fit(items, bincount=3):
items = sorted(items, reverse=1) # New - improves first fit.
bins = [[] for c in range(bincount)]
binsizes = [0] * bincount
for item in items:
minbinindex = binsizes.index(min(binsizes))
bins[minbinindex].append(item)
binsizes[minbinindex] += item
average = sum(binsizes) / float(bincount)
maxdeviation = max(abs(average - bs) for bs in binsizes)
return bins, binsizes, average, maxdeviation
def swap1(columns, colsize, average, margin=0):
'See if you can do a swap to smooth the heights'
colcount = len(columns)
maxdeviation, i_a = max((abs(average - cs), i)
for i,cs in enumerate(colsize))
col_a = columns[i_a]
for pic_a in set(col_a): # use set as if same height then only do once
for i_b, col_b in enumerate(columns):
if i_a != i_b: # Not same column
for pic_b in set(col_b):
if (abs(pic_a - pic_b) > margin): # Not same heights
# new heights if swapped
new_a = colsize[i_a] - pic_a + pic_b
new_b = colsize[i_b] - pic_b + pic_a
if all(abs(average - new) < maxdeviation
for new in (new_a, new_b)):
# Better to swap (in-place)
colsize[i_a] = new_a
colsize[i_b] = new_b
columns[i_a].remove(pic_a)
columns[i_a].append(pic_b)
columns[i_b].remove(pic_b)
columns[i_b].append(pic_a)
maxdeviation = max(abs(average - cs)
for cs in colsize)
return True, maxdeviation
return False, maxdeviation
def printit(columns, colsize, average, maxdeviation):
print('columns')
pp(columns)
print('colsize:', colsize)
print('average, maxdeviation:', average, maxdeviation)
print('deviations:', [abs(average - cs) for cs in colsize])
print()
if __name__ == '__main__':
## Some data
#import random
#heights = [random.randint(5, 50) for i in range(30)]
## Here's some from the above, but 'fixed'.
from pprint import pprint as pp
heights = [45, 7, 46, 34, 12, 12, 34, 19, 17, 41,
28, 9, 37, 32, 30, 44, 17, 16, 44, 7,
23, 30, 36, 5, 40, 20, 28, 42, 8, 38]
columns, colsize, average, maxdeviation = first_fit(heights)
printit(columns, colsize, average, maxdeviation)
while 1:
swapped, maxdeviation = swap1(columns, colsize, average, maxdeviation)
printit(columns, colsize, average, maxdeviation)
if not swapped:
break
#input('Paused: ')
The output:
columns
[[45, 12, 17, 28, 32, 17, 44, 5, 40, 8, 38],
[7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
[46, 34, 9, 37, 44, 30, 20, 28]]
colsize: [286, 267, 248]
average, maxdeviation: 267.0 19.0
deviations: [19.0, 0.0, 19.0]
columns
[[45, 12, 17, 28, 17, 44, 5, 40, 8, 38, 9],
[7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
[46, 34, 37, 44, 30, 20, 28, 32]]
colsize: [263, 267, 271]
average, maxdeviation: 267.0 4.0
deviations: [4.0, 0.0, 4.0]
columns
[[45, 12, 17, 17, 44, 5, 40, 8, 38, 9, 34],
[7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
[46, 37, 44, 30, 20, 28, 32, 28]]
colsize: [269, 267, 265]
average, maxdeviation: 267.0 2.0
deviations: [2.0, 0.0, 2.0]
columns
[[45, 12, 17, 17, 44, 5, 8, 38, 9, 34, 37],
[7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
[46, 44, 30, 20, 28, 32, 28, 40]]
colsize: [266, 267, 268]
average, maxdeviation: 267.0 1.0
deviations: [1.0, 0.0, 1.0]
columns
[[45, 12, 17, 17, 44, 5, 8, 38, 9, 34, 37],
[7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
[46, 44, 30, 20, 28, 32, 28, 40]]
colsize: [266, 267, 268]
average, maxdeviation: 267.0 1.0
deviations: [1.0, 0.0, 1.0]
Nice problem.
Heres the info on reverse-sorting mentioned in my separate comment below.
>>> h = sorted(heights, reverse=1)
>>> h
[46, 45, 44, 44, 42, 41, 40, 38, 37, 36, 34, 34, 32, 30, 30, 28, 28, 23, 20, 19, 17, 17, 16, 12, 12, 9, 8, 7, 7, 5]
>>> columns, colsize, average, maxdeviation = first_fit(h)
>>> printit(columns, colsize, average, maxdeviation)
columns
[[46, 41, 40, 34, 30, 28, 19, 12, 12, 5],
[45, 42, 38, 36, 30, 28, 17, 16, 8, 7],
[44, 44, 37, 34, 32, 23, 20, 17, 9, 7]]
colsize: [267, 267, 267]
average, maxdeviation: 267.0 0.0
deviations: [0.0, 0.0, 0.0]
If you have the reverse-sorting, this extra code appended to the bottom of the above code (in the 'if name == ...), will do extra trials on random data:
for trial in range(2,11):
print('\n## Trial %i' % trial)
heights = [random.randint(5, 50) for i in range(random.randint(5, 50))]
print('Pictures:',len(heights))
columns, colsize, average, maxdeviation = first_fit(heights)
print('average %7.3f' % average, '\nmaxdeviation:')
print('%5.2f%% = %6.3f' % ((maxdeviation * 100. / average), maxdeviation))
swapcount = 0
while maxdeviation:
swapped, maxdeviation = swap1(columns, colsize, average, maxdeviation)
if not swapped:
break
print('%5.2f%% = %6.3f' % ((maxdeviation * 100. / average), maxdeviation))
swapcount += 1
print('swaps:', swapcount)
The extra output shows the effect of the swaps:
## Trial 2
Pictures: 11
average 72.000
maxdeviation:
9.72% = 7.000
swaps: 0
## Trial 3
Pictures: 14
average 118.667
maxdeviation:
6.46% = 7.667
4.78% = 5.667
3.09% = 3.667
0.56% = 0.667
swaps: 3
## Trial 4
Pictures: 46
average 470.333
maxdeviation:
0.57% = 2.667
0.35% = 1.667
0.14% = 0.667
swaps: 2
## Trial 5
Pictures: 40
average 388.667
maxdeviation:
0.43% = 1.667
0.17% = 0.667
swaps: 1
## Trial 6
Pictures: 5
average 44.000
maxdeviation:
4.55% = 2.000
swaps: 0
## Trial 7
Pictures: 30
average 295.000
maxdeviation:
0.34% = 1.000
swaps: 0
## Trial 8
Pictures: 43
average 413.000
maxdeviation:
0.97% = 4.000
0.73% = 3.000
0.48% = 2.000
swaps: 2
## Trial 9
Pictures: 33
average 342.000
maxdeviation:
0.29% = 1.000
swaps: 0
## Trial 10
Pictures: 26
average 233.333
maxdeviation:
2.29% = 5.333
1.86% = 4.333
1.43% = 3.333
1.00% = 2.333
0.57% = 1.333
swaps: 4
This is the offline makespan minimisation problem, which I think is equivalent to the multiprocessor scheduling problem. Instead of jobs you have images, and instead of job durations you have image heights, but it's exactly the same problem. (The fact that it involves space instead of time doesn't matter.) So any algorithm that (approximately) solves either of them will do.
Here's an algorithm (called First Fit Decreasing) that will get you a very compact arrangement, in a reasonable amount of time. There may be a better algorithm but this is ridiculously simple.
Sort the images in order from tallest to shortest.
Take the first image, and place it in the shortest column.
(If multiple columns are the same height (and shortest) pick any one.)
Repeat step 2 until no images remain.
When you're done, you can re-arrange the elements in the each column however you choose if you don't like the tallest-to-shortest look.
Here's one:
// Create initial solution
<run First Fit Decreasing algorithm first>
// Calculate "error", i.e. maximum height difference
// after running FFD
err = (maximum_height - minimum_height)
minerr = err
// Run simple greedy optimization and random search
repeat for a number of steps: // e.g. 1000 steps
<find any two random images a and b from two different columns such that
swapping a and b decreases the error>
if <found>:
swap a and b
err = (maximum_height - minimum_height)
if (err < minerr):
<store as best solution so far> // X
else:
swap two random images from two columns
err = (maximum_height - minimum_height)
<output the best solution stored on line marked with X>

Resources