2D convolution with different kernel per location

jhultman: <p>Math generally is less ambiguous than words so let me just show some equations of what I’m trying to implement. (1) is standard 2D convolution with unit batch size, 3x3 kernel size, and unit stride, and (2) is what I want to implement.</p>
<p><div class="lightbox-wrapper"><a class="lightbox" href="https://discuss.pytorch.org/uploads/default/original/2X/6/6d70c57bb63650304c5008b4f6c5514b64fb6e7f.png" data-download-href="https://discuss.pytorch.org/uploads/default/6d70c57bb63650304c5008b4f6c5514b64fb6e7f" title="image"><img src="https://discuss.pytorch.org/uploads/default/optimized/2X/6/6d70c57bb63650304c5008b4f6c5514b64fb6e7f_2_517x165.png" alt="image" data-base62-sha1="fC9EFoq3K3kMZ0zu3UgSpQYlvFZ" width="517" height="165" srcset="https://discuss.pytorch.org/uploads/default/optimized/2X/6/6d70c57bb63650304c5008b4f6c5514b64fb6e7f_2_517x165.png, https://discuss.pytorch.org/uploads/default/optimized/2X/6/6d70c57bb63650304c5008b4f6c5514b64fb6e7f_2_775x247.png 1.5x, https://discuss.pytorch.org/uploads/default/optimized/2X/6/6d70c57bb63650304c5008b4f6c5514b64fb6e7f_2_1034x330.png 2x" data-dominant-color="F8F8F8"><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">image</span><span class="informations">1191×383 16.4 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>In equation (2), the kernel <code>w</code> is modulated via element-wise product by another tensor <code>d</code> whose values depend on both the spatial index <code>(i, j)</code> as well as the dummy “convolution index” <code>(m, n)</code>. Note that the convolutional kernel <code>w</code> is learnable while the modulating tensor <code>d</code> is known. Another way to formulate this is to use <code>d</code> and <code>w</code> to form a new kernel of shape <code>(3, 3, H, W, K_in, K_out)</code> whose values depend not just on the convolution index <code>(m, n)</code> but also on the spatial index <code>(i, j)</code>.</p>
<p>Is there a simple way to express this computation without rewriting the CUDA kernels for conv2d?</p>

michaelklachko: <p>Don’t know if it’s possible to use conv2d for this, but maybe play with fold/unfold ops to avoid writing cuda kernel: <a href="https://pytorch.org/docs/stable/_modules/torch/nn/modules/fold.html" rel="nofollow noopener">https://pytorch.org/docs/stable/_modules/torch/nn/modules/fold.html</a></p>

jhultman: <p>Thanks so much for this suggestion. Unfold + matmul + fold worked flawlessly.</p>

Oktai15: <p>Unfold is right solution, but it is super slow. Is there any idea how to resolve it?</p>

michaelklachko: <p>It should be about the same speed as the regular conv layer if you perform all dot products in parallel.</p>

fschiffers: <p>Does anyone have some example code where such a “convolution” is implemented with the Fold operation?</p>

ptrblck: <p>The <a href="https://pytorch.org/docs/stable/generated/torch.nn.Unfold.html?highlight=unfold#torch.nn.Unfold"><code>nn.Unfold</code> docs</a> provide an example:</p>
<pre><code class="lang-python">unfold = nn.Unfold(kernel_size=(2, 3))
input = torch.randn(2, 5, 3, 4)
output = unfold(input)
# each patch contains 30 values (2x3=6 vectors, each of 5 channels)
# 4 blocks (2x3 kernels) in total in the 3x4 input
output.size()

# Convolution is equivalent with Unfold + Matrix Multiplication + Fold (or view to output shape)
inp = torch.randn(1, 3, 10, 12)
w = torch.randn(2, 3, 4, 5)
inp_unf = torch.nn.functional.unfold(inp, (4, 5))
out_unf = inp_unf.transpose(1, 2).matmul(w.view(w.size(0), -1).t()).transpose(1, 2)
out = torch.nn.functional.fold(out_unf, (7, 8), (1, 1))
# or equivalently (and avoiding a copy),
# out = out_unf.view(1, 2, 7, 8)
(torch.nn.functional.conv2d(inp, w) - out).abs().max()
</code></pre>

fschiffers: <p>Thanks!</p>
<p>I’ve been trying to implement a Gaussian-like filter where the spread of each Gaussian depends on the intensity of the actual image pixel. In other words: If the pixel intensity is close to 0, the pixel shouldn’t blur, if the value is large (close to 1) it should blur a lot.</p>
<p>Creating the Gaussian kernel for each image pixel is simple, but I haven’t managed to find a way to combine my kernels with the unfolded image in a similar way as it was done in the example above.</p>

mikonvergence: <p>I don’t know if this is what you were looking for, but I created this tiny piece of code to perform something similar.</p>
<aside class="onebox allowlistedgeneric" data-onebox-src="https://github.com/mikonvergence/LocalGaussianBlur">
  <header class="source">
      <img src="https://github.githubassets.com/favicons/favicon.svg" class="site-icon" width="16" height="16">

      <a href="https://github.com/mikonvergence/LocalGaussianBlur" target="_blank" rel="noopener nofollow ugc">GitHub</a>
  </header>

  <article class="onebox-body">
    <div class="aspect-image" style="--aspect-ratio:690/345;"><img src="https://opengraph.githubassets.com/7b4be44206f8fe4d3800c757e67a51df740b3c2db26f23839441b0ef59f78572/mikonvergence/LocalGaussianBlur" class="thumbnail" width="690" height="345"></div>

<h3><a href="https://github.com/mikonvergence/LocalGaussianBlur" target="_blank" rel="noopener nofollow ugc">GitHub - mikonvergence/LocalGaussianBlur: PyTorch implementation of a...</a></h3>

  <p>PyTorch implementation of a Gaussian Blur operation with changing kernel variance - GitHub - mikonvergence/LocalGaussianBlur: PyTorch implementation of a Gaussian Blur operation with changing kerne...</p>


  </article>

  <div class="onebox-metadata">
    
    
  </div>

  <div style="clear: both"></div>
</aside>