Translating Pytorch program into Keras: different results
I have translated a pytorch program into keras.

A working Pytorch program:

import numpy as np
import cv2
import torch
import torch.nn as nn
from skimage import segmentation

np.random.seed(1)
torch.manual_seed(1)


fi = "in.jpg"


class MyNet(nn.Module):
    def __init__(self, n_inChannel, n_outChannel):
        super(MyNet, self).__init__()

        self.seq = nn.Sequential(
            nn.Conv2d(n_inChannel, n_outChannel, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(n_outChannel),

            nn.Conv2d(n_outChannel, n_outChannel, kernel_size=3, stride=1, padding=1),
            nn.ReLU(inplace=True),
            nn.BatchNorm2d(n_outChannel),

            nn.Conv2d(n_outChannel, n_outChannel, kernel_size=1, stride=1, padding=0),
            nn.BatchNorm2d(n_outChannel)
        )

    def forward(self, x):
        return self.seq(x)


im = cv2.imread(fi)
data = torch.from_numpy(np.array([im.transpose((2, 0, 1)).astype('float32')/255.]))
data = data.cuda()

labels = segmentation.slic(im, compactness=100, n_segments=10000)
labels = labels.flatten()
u_labels = np.unique(labels)
label_indexes = np.array([np.where(labels == u_label)[0] for u_label in u_labels])

n_inChannel = 3
n_outChannel = 100

model = MyNet(n_inChannel, n_outChannel)
model.cuda()
model.train()
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)

label_colours = np.random.randint(255,size=(100,3))

for batch_idx in range(100):
    optimizer.zero_grad()
    output = model( data )[ 0 ]
    output = output.permute( 1, 2, 0 ).view(-1, n_outChannel)
    ignore, target = torch.max( output, 1 )

    im_target = target.data.cpu().numpy()
    nLabels = len(np.unique(im_target))

    im_target_rgb = np.array([label_colours[ c % 100 ] for c in im_target]) # correct position of "im_target"
    im_target_rgb = im_target_rgb.reshape( im.shape ).astype( np.uint8 )

    for inds in label_indexes:
        u_labels_, hist = np.unique(im_target[inds], return_counts=True) 
        im_target[inds] = u_labels_[np.argmax(hist, 0)]

    target = torch.from_numpy(im_target)
    target = target.cuda()    

    loss = loss_fn(output, target)

    loss.backward()
    optimizer.step()

    print (batch_idx, '/', 100, ':', nLabels, loss.item())

    if nLabels &lt;= 3:
        break         
fo = "out.jpg"
cv2.imwrite(fo, im_target_rgb)


(source: https://github.com/kanezaki/pytorch-unsupervised-segmentation/blob/master/demo.py)

My translation into Keras:

import cv2
import numpy as np
from skimage import segmentation
from keras.layers import Conv2D, BatchNormalization, Input, Reshape
from keras.models import Model
import keras.backend as k
from keras.optimizers import SGD, Adam
from skimage.util import img_as_float
from skimage import io
from keras.models import Sequential

np.random.seed(0)

fi = "in.jpg"
im = cv2.imread(fi).astype(float)/255.

labels = segmentation.slic(im, compactness=100, n_segments=10000)
labels = labels.flatten()
print (labels.shape)
u_labels = np.unique(labels)

label_indexes = [np.where(labels == u_label)[0] for u_label in np.unique(labels)]

n_channels = 100

model = Sequential()

model.add (  Conv2D(n_channels, kernel_size=3, activation='relu', input_shape=im.shape, padding='same'))    
model.add( BatchNormalization())

model.add( Conv2D(n_channels, kernel_size=3, activation='relu', padding='same'))    
model.add( BatchNormalization())

model.add( Conv2D(n_channels, kernel_size=1, padding='same'))
model.add( BatchNormalization())

model.add( Reshape((im.shape[0] * im.shape[1], n_channels)))

img = np.expand_dims(im,0)
print (img.shape)
output = model.predict(img)

print (output.shape)

im_target = np.argmax(output[0], 1)
print (im_target.shape)

for inds in label_indexes:
    u_labels_, hist = np.unique(im_target[inds], return_counts=True)
    im_target[inds] = u_labels_[np.argmax(hist, 0)]

def custom_loss(loss_target, loss_output):
  return k.categorical_crossentropy(target=k.stack(loss_target), output=k.stack(loss_output), from_logits=True)


model.compile(optimizer=SGD(lr=0.1, momentum=0.9), loss=custom_loss)

model.fit(img, output, epochs=100, batch_size=1, verbose=1)

pred_result = model.predict(x=[img])[0]
print (pred_result.shape)

target = np.argmax(pred_result, 1)
print (target.shape)

nLabels = len(np.unique(target))

label_colours = np.random.randint(255, size=(100, 3))

im_target_rgb = np.array([label_colours[c % 100] for c in im_target])

im_target_rgb = im_target_rgb.reshape(im.shape).astype(np.uint8)

cv2.imwrite("out.jpg", im_target_rgb)


However, Keras output is really different than of pytorch

Input image:



Pytorch result:



Keras result:



Could someone help me for this translation?  

Edit 1:

I corrected two errors as advised by @sebrockm
1. removed `relu` from last conv layer
2. added `from_logits = True` in the loss function


Also, changed the no. of conv layers from 4 to 3 to match with the original code. 

However, output image did not improve than before and the `loss` are resulted in negative:

Epoch 99/100
1/1 [==============================] - 0s 92ms/step - loss: -22.8380

Epoch 100/100    
1/1 [==============================] - 0s 99ms/step - loss: -23.039


I think that the Keras code lacks connection between model and output. However, could not figure out to make this connection.

Two major mistakes that I see (likely related):


The last convolutional layer in the original model does not have an activation function, while your translation uses relu.
The original model uses CrossEntropyLoss as loss function, while your model uses categorical_crossentropy with logits=False (a default argument). Without mathematical background the difference is tricky to explain, but in short: CrossEntropyLoss has a softmax built in, that's why the model doesn't have one on the last layer. To do the same in keras, use k.categorical_crossentropy(..., logits=True). "logits" means the input values are expected not to be "softmaxed", i.e. all values can be arbitrary. Currently, your loss function expects the output values to be "softmaxed", i.e. all values must be between 0 and 1 (and sum up to 1).


Update:

One other mistake, likely a huge one: In Keras, you calculate the output once in the beginning and never change it from there on. Then you train your model to fit on this initially generated output.

In the original pytorch code, target (which is the variable being trained on) gets updated in every training loop. 
So, you cannot use Keras' fit method which is designed for doing the entire training for you (given fixed training data). You will have to replicate the training loop manually, just as it is done in the pytorch code. I'm not sure if this is easily doable with the API Keras provides. train_on_batch is one method you surely will need in your manual loop. You will have to do some more work, I'm afraid...
