Autoencoder: Reconstructed images incorrect after denormalization

nwn: <p>Hello there,<br>
I am currently dealing with image reconstruction using a simple Convolutional Autoencoder. The Autoencoder is split into two networks:</p>
<ol>
<li>The Encoder: Maps the input image to latent space with ReLU as last activation function</li>
<li>The Decoder: Maps the latent variable back to the image space. Last activation function is tanh or sigmoid.</li>
</ol>
<p>The data preprocessing step is:  [transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))] The mean and std values were calculated channel wise on the training set.<br>
To denormalize/ unnormalize the images, I used the following code which is closely related to <a href="https://discuss.pytorch.org/t/simple-way-to-inverse-normalize-a-batch-of-input-variable/12385">Unnormalize images</a>:</p>
<p>def denormalize(data, mean, std):<br>
data_un = data.new(*data.size())<br>
n_channel = data.shape[1]<br>
for c in n_channel:<br>
data_un[:,c] = data[:,c]*std[c] + mean[c]<br>
return data_un</p>
<p>When I train the network (with MSE loss on MNIST) and look at the reconstructed images they do not match, although the general structure looks very similar. Note that as activation functions in the final layer of the decoder, I tried both: sigmoid and tanh. After the normalization step of the input data, the images are shifted, so that they are in [-1,1]. For that reason, I thought it would make more sense to use an Tanh instead of a Sigmoid. However, when I denormalize the reconstructed images, they look off.<br>
The first figure shows the sigmoid example, the latter figure displays the tanh example.<br>
Note that, the first row shows the input images after they have been normalized and unnormalized, the second row shows the same images after reconstruction and unormalization.<br>
Sigmoid:<br>
<div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/9/2/92a4b7aaf03a573858566de3560bad09c337d997.png" data-download-href="https://discuss.pytorch.org/uploads/default/92a4b7aaf03a573858566de3560bad09c337d997" title="img_grid_sigmoid_26"><img src="https://discuss.pytorch.org/uploads/default/original/3X/9/2/92a4b7aaf03a573858566de3560bad09c337d997.png" alt="img_grid_sigmoid_26" data-base62-sha1="kVgCcFtOysOa5QPvrP819Mv00Vp" width="690" height="180" data-dominant-color="2B2B2B"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">img_grid_sigmoid_26</span><span class="informations">847×222 15 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div><br>
Tanh:<br>
<div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/3X/6/c/6ca88e5b787843ecf25c78ae1a77262ebf4ebf9f.png" data-download-href="https://discuss.pytorch.org/uploads/default/6ca88e5b787843ecf25c78ae1a77262ebf4ebf9f" title="img_grid_tanh_26"><img src="https://discuss.pytorch.org/uploads/default/original/3X/6/c/6ca88e5b787843ecf25c78ae1a77262ebf4ebf9f.png" alt="img_grid_tanh_26" data-base62-sha1="fveHbxG8Q4jgOvaYZLSN9ghFesn" width="690" height="181" data-dominant-color="353535"><div class="meta"><svg class="fa d-icon d-icon-far-image svg-icon" aria-hidden="true"><use href="#far-image"></use></svg><span class="filename">img_grid_tanh_26</span><span class="informations">838×220 24.9 KB</span><svg class="fa d-icon d-icon-discourse-expand svg-icon" aria-hidden="true"><use href="#discourse-expand"></use></svg></div></a></div></p>
<p>The code below was used to log the images to Tensorboard:<br>
dec_out: decoder output<br>
data_nm: the normalized images given as input to the encoder<br>
images_normalized = torch.concat([data_nm[:8, ].detach(), dec_out[:8, ].detach()])<br>
images_unnormalized = denormalize(data=images_normalized, mean=(0.1307,), std=(0.3081,))<br>
img_grid = make_grid(images_unnormalized, nows=8)</p>
<p>Do you have any idea why this happens and whether it is a good idea to denormalize the reconstructed images? Does it make sense to scale the input images after normalization back to [0,1]? Or is there something else that I am missing?<br>
Thank you for any hints <img src="https://discuss.pytorch.org/images/emoji/apple/slight_smile.png?v=12" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"></p>

usmancheema89: <aside class="quote no-group" data-username="nwn" data-post="1" data-topic="168248">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/n/a3d4f5/48.png" class="avatar"> nwn:</div>
<blockquote>
<p>structure looks very similar. Note that as activation functions in the final layer of the decoder, I tried both: sigmoid and tanh. After the normalization step of the input data, the images are shifted, so that they are in [-1,1]. For that reason, I thought it would make more sense to use an Tanh instead of a Sigmoid. However, when I denormalize the reconstructed images, they look off.<br>
The first figure shows the sigmoid example, the latter figure displays the tanh example.<br>
Note that, the first row shows the input images after they have been normalized and unnormalized, the second row shows the same images after reconstruction and unormalization.<br>
Sigmoid:</p>
</blockquote>
</aside>
<p>Instead of Tanh or Sigmoid or data mean and std to denorm the image, I would suggest using output’s min and max to denorm them</p>
<p>max = np.max(img)<br>
min = np.min(img)<br>
denormed_img = (img - min) / (max - min) * 255.0</p>

nwn: <p>Thank your for answering. So, would you make the last layer linear and then apply your above mentioned way? Please elaborate on your answer and your thoughts behind it.</p>

usmancheema89: <p>Sigmoid scales the data between 0 and 1<br>
Tanh scales the data between -1 and 1<br>
The mean and std of the image are fine to scale the data as input but the output should be scaled the same way as your input to the loss.<br>
I guess what I’m trying to explain (very badly) is that when you calculate the loss between the input image and output image, the network should be able to give in output in the same range as the input.<br>
I would scale the input images between -1 and 1, use linear or tanh as layer of the network, and compute loss between input and output (both should be in -1 to 1 range now)<br>
To visualize you can use the formula shared earlier</p>

nwn: <p>This was a great hint, thank you ! Maybe a quick update from my side and a few clearifications:</p>
<ol>
<li>Wrongfully, I didn’t check the range over the dataset after the normalization step: the real range is in [~-0.42, ~2.82].</li>
<li>I tried unnormalizing the images with the formula that you have provided <strong>and it works fine</strong>. Now, I individually scale every image by its min and max.The images that I get look very similar, although a bit brighter then the originals. It is not required to scale them back to [0, 255] because (in my case) tensorboard can handle ranges in [0,1].</li>
<li>So you would not normalize the data, so that is has 0 mean and an std of 1; rather rescale it?</li>
</ol>

usmancheema89: <p>As per my understanding, the basis for scaling comes from two reasons<br>
a) when you have data from different types eg house prices, house areas, and the number of rooms, you want all the information in the same range. In such a case 0 mean std 1 makes sense because you don’t have a bound or range of data (or rather multiple ranges for each type of data)<br>
b) you want to scale the images in a small range so that the network does not have to deal with large numbers (avoid exploding gradients) for stability.</p>
<p>When it comes to images I have seen people scale the data 0 to 1, -1 to 1, 0 mean std 1, or use the image net mean and std to scale the data. All of the approaches work fine in different cases.</p>
<p>To me, it seemed intuitive that you want to scale the output of the network to occupy the max range of color space (0-255) for best-looking results.</p>
<p>I know that’s not the exact answer to why, but hopefully gives you some insights into normalization</p>

usmancheema89: <p>rescaling and normalization can be the same if you use the right mean and std <img src="https://discuss.pytorch.org/images/emoji/apple/stuck_out_tongue.png?v=12" title=":stuck_out_tongue:" class="emoji" alt=":stuck_out_tongue:" loading="lazy" width="20" height="20"></p>

nwn: <p>Thanks. I mostly worked in domains where the algorithms had the explicit assumption that the mean and std are 0,1, respectively. IMHO using the imagenet values is a bit misleading because it works fine for most mondane domains but leaves out custom or highly specific datasets.</p>

usmancheema89: <aside class="quote no-group" data-username="nwn" data-post="8" data-topic="168248">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/n/a3d4f5/48.png" class="avatar"> nwn:</div>
<blockquote>
<p>IMHO using the imagenet values is a bit misleading</p>
</blockquote>
</aside>
<p>I agree, I have trained a single network on a mix of thermal and visible images, I found that calculating the mean and std over both modalities gave me the best results.</p>