Hello Andreas!

ConvTranspose2d should do what you want (at least in the plain-vanilla,

2d case – I didn’t check 3d or various stride / padding combinations).

Here is an example script:

```
import torch
torch.__version__
torch.random.manual_seed (2020)
cnva = torch.nn.Conv2d (1, 1, 3, padding = 1, bias = False)
cnvt = torch.nn.ConvTranspose2d (1, 1, 3, padding = 1, bias = False)
with torch.no_grad():
_ = cnvt.weight.copy_ (cnva.weight)
s = torch.randn ((1, 1, 7, 7))
t = torch.randn ((1, 1, 7, 7))
(cnva (s) * t).sum()
(s * cnvt (t)).sum()
```

Note the use of the *same* weight matrix for `ConvTranspose2d`

as for

`Conv2d`

.

And here is its output:

```
>>> import torch
>>> torch.__version__
'1.6.0'
>>> torch.random.manual_seed (2020)
<torch._C.Generator object at 0x7efc1f81c6f0>
>>>
>>> cnva = torch.nn.Conv2d (1, 1, 3, padding = 1, bias = False)
>>> cnvt = torch.nn.ConvTranspose2d (1, 1, 3, padding = 1, bias = False)
>>> with torch.no_grad():
... _ = cnvt.weight.copy_ (cnva.weight)
...
>>> s = torch.randn ((1, 1, 7, 7))
>>> t = torch.randn ((1, 1, 7, 7))
>>> (cnva (s) * t).sum()
tensor(3.5478, grad_fn=<SumBackward0>)
>>> (s * cnvt (t)).sum()
tensor(3.5478, grad_fn=<SumBackward0>)
```

To understand the adjoint, think of a 2d 100x100 image as being a

vector of length 1,000,000. The 2d convolution can be represented

(inefficiently) as a 1,000,000 x 1,000,000 matrix with a particular

banded structure. Because it’s real, its adjoint is just its transpose.

`ConvTranspose2d`

represents (algorithmically) the appropriate

banded structure for that transposed 1,000,000 x 1,000,000 matrix.

(If pytorch were to support convolutions with complex weights, you

would presumably use the complex conjugate of `Conv2d`

's weights

for `ConvTranspose2d`

.)

Good luck.

K. Frank