How the gradient of a row vector w.r.t. another row vector is calculated via Jacobian-vector-product?

username: oat
cooked: <p><strong>[ 1. calculate gradient via backward() ]</strong><br>
The following code generates the <strong>gradient</strong> of the output of a <strong>row-vector-valued function</strong> y with respect to (w.r.t.) its <strong>row-vector input</strong> x, using the <code>backward()</code> function in <code>autograd</code>.</p>
<p>(Strictly speaking, x and y are both <strong>1xN matrix</strong>, and this is why the Jacobian matrix is a 1x2x1x2 tensor before it is “squeezed” as shown below in section 2, because this is a Jacobian of <em>one matrix</em> w.r.t. <em>another matrix</em>)</p>
<pre><code class="lang-auto">x = torch.tensor( [[2, 3]], dtype=torch.float, requires_grad=True)

def func(x):
    y = torch.zeros(1, 2) 
    y[0, 0] = x[0, 0]**2 + 3*x[0, 1] 
    y[0, 1] = x[0, 1]**2 + 2*x[0, 0]
    return y 

y = func(x)

y.backward(gradient=torch.ones_like(y))

x.grad
</code></pre>
<p>The output is:<br>
<code>tensor([[6., 9.]])</code></p>
<p><strong>[ 2. calculate gradient manually via Jacobian-vector product ]</strong><br>
However, I’m unable to obtain the gradient of x mannually using the <strong>Jacobian-vector product</strong> method as shown below, i.e. conducting <strong>matrix multiplication</strong> between the <strong>transpose of the Jacobian matrix</strong> and <strong>a vector of “ones” in the same shape of y (i.e. a 1x2 row vector)</strong>:</p>
<pre><code class="lang-auto">x = torch.tensor( [[2, 3]], dtype=torch.float, requires_grad=True)

def func(x):
  y = torch.zeros(1, 2)
  y[0, 0] = x[0, 0]**2 + 3&lt;em&gt;x[0, 1]
  y[0, 1] = x[0, 1]**2 + 2&lt;/em&gt;x[0, 0]
  return y

y = func(x)

J = torch.squeeze(torch.autograd.functional.jacobian(func, x))

x_grad = torch.matmul(
  torch.transpose(J, 0, 1) , 
  torch.ones_like(y)
)

x_grad
</code></pre>
<p>This is because the <strong>2x2 Jacobian matrix of y w.r.t. x</strong> cannot multiply a <strong>1x2 row vector of ones</strong> in the same shape of y, as indicated in the error message, which is understandable.</p>
<pre><code class="lang-auto">RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x2 and 1x2)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
&lt;ipython-input-33-3ebd031d82e3&gt; in &lt;module&gt;
     11 J = torch.squeeze(torch.autograd.functional.jacobian(func, x))
     12 
---&gt; 13 x_grad = torch.matmul(
     14     torch.transpose(J, 0, 1),
     15     torch.ones_like(y)
RuntimeError: mat1 and mat2 shapes cannot be multiplied (2x2 and 1x2)
</code></pre>
<p><strong>[ 3. My Question ]</strong><br>
So, if the gradient is indeed calculated by <code>autograd</code> using the <strong>Jacobian-vector-product</strong> method, does this mean that <strong>internally</strong> <code>autograd</code> will <strong>convert(transpose)</strong> the <strong>input row vector of ones</strong> to <strong>a column vector in the same length</strong> so that the matrix multiplication can be conducted correctly, and the resulting <strong>column vector</strong> will be <strong>transposed</strong> so that the final output is <strong>a row vector in the same shape as x</strong>, as shown below?</p>
<pre><code class="lang-auto">x = torch.tensor( [[2, 3]], dtype=torch.float, requires_grad=True)

def func(x):
    y = torch.zeros(1, 2) 
    y[0, 0] = x[0, 0]**2 + 3*x[0, 1] 
    y[0, 1] = x[0, 1]**2 + 2*x[0, 0]
    return y 

y = func(x)

J = torch.squeeze(torch.autograd.functional.jacobian(func, x))

x_grad = torch.transpose(torch.matmul(
    torch.transpose(J, 0, 1),
    torch.transpose(torch.ones_like(y), 0, 1) # transpose the row vector of ones to a column vector
), 0, 1)  # the result is transposed back to a row vector in the same shape as x

x_grad
</code></pre>
<p>… which does generate the correct results:<br>
<code>tensor([[6., 9.]])</code></p>

username: tom
cooked: <p>While the shapes and maths work out in your case (so the answer to your question is could be just “yes”), the general answer is a bit more elaborate:<br>
As you note, autograd uses backpropagation to track the Jacobian-vector-product. However, if you want to interpret this literally as a matrix-vector-product of the Jacobian with the vector, PyTorch  - as would be usual for systems implementing this - does not follow the “mathematical shapes” (vector and matrix) literally.<br>
In backpropagation, we compute the JVP of a composition f ○ g at an input x as D(f ○ g) v = Df (Dg v). Let us say that y = f(x) and z = g(y).<br>
Now v would always be a column vector here as would (Dg v). In PyTorch, however, the convention is that this conceptual column vector is represented by rearranging the entries to the shape of the (intermediate) output, so v will be represented as a tensor of the same shape shape as z and (Dg v) would have the same shape as y. The matrices themselves are typically not explicit either (with matrix multiplication being the big exception), because this would be very inefficient:<br>
Consider a function f multiplying a 3 x 3 x 3 tensor by two. The output is again 3 x 3 x 3 so the vector v in the JVP Df v would be 27 entries So would the JVP itself, and the matrix Df would be a 27 x 27 matrix with 2 on the diagonal and zeros elsewhere, but of course, autograd would not compute it by building that matrix but would just multiply v with 2 to apply Df and get (Df v) = 2v.</p>
<p>If we were maths-braggards we might alternatively describe this as not operating on vectors but on elements in the dual space of the domain and image space (as a finite-dimensional vector spaces over the reals) which we identify with the primal space and the Jacobian D being a contravariant functor mapping each function f : X → Y to Df : D(Y)=Y → D(X)=X (note that because we operate on the dual spaces, we pull back the dual space element using the more conventional linear map between the tangent spaces from, say, functional analysis), and there will be no vectors and instead congratulations for the clever use of category theory in programming. But in the end while it might be good to know that there is some more fancy abstract stuff in the background rather than everything being a fragile ad-hoc construction this doesn’t add that much value here and we are not maths-braggards, are we?</p>
<p>Best regards</p>
<p>Thomas</p>