Convolution with several kernels on the same input image

pierremrg: <p>Hi all,</p>
<p>I’m performing cross-correlations between images and kernels. But it’s common that I need to perform a cross-correlation on an <strong>unique image</strong> with many <strong>different kernels</strong>.</p>
<p>All kernels have the same shape, so a simple solution is to batch them, and duplicate the image along the batch dimension. Doing so, I have the following tensors:</p>
<pre><code class="lang-auto">images batch of shape [n_kernels, n_channels, img_h, img_w]
kernels batch of shape [n_kernels, n_channels, krn_h, krn_w]
</code></pre>
<p>The cross-correlation operation works as expected, but I will run out of memory quickly: if my images’ size is 200x200px with 128 channels, if I have 50 kernels, I will need to store 200x200x128x50 floats in memory… which is huge.</p>
<p>Is there any way to perform my cross-correlation operations without duplicating my input image?</p>
<p>Thank you <img src="https://discuss.pytorch.org/images/emoji/apple/slight_smile.png?v=10" title=":slight_smile:" class="emoji" alt=":slight_smile:"></p>

ptrblck: <p>I’m unsure I understand the use case correctly and don’t know why repeating the image in the batch dimension would be necessary. Could you post a code snippet showing the desired results, please?</p>

pierremrg: <p>Sure, here is the code snippet I use:</p>
<pre><code class="lang-auto">	batch = 3
	channels = 2

	# Define a sample image (the **same** image is batched 3 times)
	f_img = torch.zeros((batch, channels, 5, 10))
	# pattern is a small 3x3 pattern for which I'm looking for with my cross-correlation
	# pattern content and position are arbitrary for the tests
	f_img[:, :, 2:5, 6:9] = pattern 
	f_img[:, :, 1:4, 1:4] = pattern * 2

	# Define 3 **different** kernel
	f_krn = torch.zeros((batch, channels, 3, 3))
	f_krn[0, :, :, :] = pattern
	f_krn[1, :, :, :] = pattern * 2
	f_krn[2, :, :, :] = pattern * 3

	print(f_img.shape, f_krn.shape)
	# Image shape: [3, 2, 5, 10] / Kernel shape: [3, 2, 3, 3]

	# Perform cross-correlation
	f_img = f_img.view(1, batch * channels, f_img.shape[2], f_img.shape[3])
	f_krn = f_krn.view(batch, channels, 3, 3)

	result = F.conv2d(f_img, f_krn, groups=batch)

	print(result)
</code></pre>
<p>Thanks</p>

ptrblck: <p>Thanks for the code snippet.<br>
Your approach looks valid assuming you are using different images in <code>f_img</code>.<br>
However, based on your previous description:</p>
<aside class="quote no-group" data-username="pierremrg" data-post="1" data-topic="129582">
<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/p/b2d939/48.png" class="avatar"> pierremrg:</div>
<blockquote>
<p>But it’s common that I need to perform a cross-correlation on an <strong>unique image</strong> with many <strong>different kernels</strong>.</p>
<p>All kernels have the same shape, so a simple solution is to batch them, and duplicate the image along the batch dimension.</p>
</blockquote>
</aside>
<p>it seems that <code>f_img</code> would contain only a single unique image and you are repeating it in the batch dimension to use the <code>view</code> operation with the grouped conv approach.<br>
Your grouped conv would create 3 groups (basically splitting the channels into the “images”) and would then apply each corresponding conv kernel to the input.<br>
If so, you should be able to use a plain convolution to get the same result, if I’m not mistaken.<br>
This workflow would use a single image and each filter would still be applied to it.<br>
To do so, I’ve changed your code a bit to really repeat the input image in the batch dimension and initialized the filter kernels randomly.<br>
Could you check, if I understand your use case correctly?</p>
<pre data-code-wrap="python"><code class="lang-python">batch = 3
channels = 2

#define a sample image (the **same** image is batched 3 times)
f_img = torch.randn((1, channels, 5, 10)).repeat(batch, 1, 1, 1)

pattern = 1.
# Define 3 **different** kernel
f_krn = torch.randn((batch, channels, 3, 3))
f_krn[0, :, :, :] = pattern
f_krn[1, :, :, :] = pattern * 2
f_krn[2, :, :, :] = pattern * 3

print(f_img.shape, f_krn.shape)
# Image shape: [3, 2, 5, 10] / Kernel shape: [3, 2, 3, 3]

# Perform cross-correlation
f_img_ = f_img.view(1, batch * channels, f_img.shape[2], f_img.shape[3])
f_krn = f_krn.view(batch, channels, 3, 3)

result = F.conv2d(f_img_, f_krn, groups=batch)

# single image
out = F.conv2d(f_img[0:1], f_krn)
print((out == result).all())
&gt; tensor(True)
</code></pre>

pierremrg: <p>You understood the question perfectly!</p>
<p>In fact, I didn’t know it was possible to use a plain convolution with a different batch size between the image and the kernel. I was pretty sure that, having a batch size of 3 for my kernel, my image needed to have a batch size of 3 too.</p>
<p>(Just in case, your last line should be <code>print((abs(out - result) &lt; 1e-6).all())</code> - surely because of floating precision).</p>
<p>Thanks for your help, your answers are always useful, whatever the subject!</p>

ptrblck: <p>Ah OK, yeah your explanation is right. The filter shapes is only defined by the number of kernels, the input channels, as well as the spatial size of the kernels and is thus independent from the batch size. This allows you to create models accepting different batch sizes (e.g. during training you could use a larger batch size while the deployed model could accept a single image).</p>
<p>Also yes, the better approach would be to use a small <code>eps</code> value or <code>torch.allclose</code>.</p>