• 0. Linear operator
  • 1. Gradient of a function
  • 2. Jacobian of a function
  • 3. Key matrix derivatives
  • 4. Function is a DAG
  • 5. Autodiff on a DAG
  • 6. VJP and JVP
  • 7. Hessian of a function
unitgridOpacitycanvasColororiginXoriginY

Linear operator

  • 0. Linear operator
  • 1. Product rule and chain rule
  • 2. Some examples
    • 2.1. f(x)=∥x∥2f(x) = \lVert x \rVert^2f(x)=∥x∥2 where xxx is a vector
    • 2.2. f(X)=Xnf(X) = X^nf(X)=Xn
    • 2.3. f(X)=X−1f(X) = X^{-1}f(X)=X−1
    • 2.4. f(Q)=QQTf(Q) = QQ^Tf(Q)=QQT where QQQ is an orthonormal matrix
  • 3. Closing example

Let's start with the central idea of calculus: a small change in output dydydy given a small change in input dxdxdx, for a function y=f(x)y = f(x)y=f(x) where x,yx,yx,y can be scalars or vectors:

dy=f(x+dx)−f(x)dy = f(x+dx) - f(x)dy=f(x+dx)−f(x)

Canceling the higher order terms involving dxdxdx leaves dydydy as a linear function of dxdxdx. As an example, consider Y=X2=XXY = X^2 = XXY=X2=XX:

dY=f(X+dX)−f(X)=(X+dX)(X+dX)−X2=X2+X(dX)+(dX)X+(dX)(dX)⏟higher order−X2=X(dX)+(dX)X=X(dX)+(dX)X\begin{aligned} dY &= f(X+dX) - f(X) \\ &= (\textcolor{#1d4ed8}{X} + \textcolor{#be185d}{dX})(\textcolor{#1d4ed8}{X} + \textcolor{#be185d}{dX}) - \textcolor{#1d4ed8}{X^2} \\ &= \textcolor{#1d4ed8}{X^2} + \textcolor{#1d4ed8}{X}\textcolor{#be185d}{(dX)} + \textcolor{#be185d}{(dX)}\textcolor{#1d4ed8}{X} + \underbrace{\textcolor{#be185d}{(dX)}\textcolor{#be185d}{(dX)}}_{\text{higher order}} - \textcolor{#1d4ed8}{X^2} \\ &= \textcolor{#1d4ed8}{X}\textcolor{#be185d}{(dX)} + \textcolor{#be185d}{(dX)}\textcolor{#1d4ed8}{X} \\ &= \textcolor{#b45309}{X(dX) + (dX)X} \end{aligned}dY​=f(X+dX)−f(X)=(X+dX)(X+dX)−X2=X2+X(dX)+(dX)X+higher order(dX)(dX)​​−X2=X(dX)+(dX)X=X(dX)+(dX)X​

The term (dX)(dX)(dX)(dX)(dX)(dX) is dropped as higher order, and note X(dX)≠(dX)XX(dX) \ne (dX)XX(dX)=(dX)X. Though not immediately obvious, dYdYdY is a linear function of dXdXdX. For a fixed dXdXdX, dYdYdY is also a (not necessarily linear) function of XXX.


0. Linear operator

A linear operator is a linear map from one vector space to another vector space.

It has a subtle difference from a function: a function can be a map from any set to any set, but here we constrain the input and outputs to be elements of a vector space. The most common example of a linear operator is a matrix (ref: Matrix is a function). In fact, any linear operator can be represented as a matrix. However it is often more efficient to write it in the less obvious way (as in the above example). It will become clearer as we proceed.

For a (smooth) function f:Rn→Rmf: R^n \rightarrow R^mf:Rn→Rm, we can obtain a linear operator LfL_fLf​ which is a function of dXdXdX (a small change in input) and whose output is a function of XXX (a small change in output dYdYdY at the point XXX).

pie.clear()
pie.scalar(50, out='unit')
pie.scalar(0, out='gridOpacity')
TEAL, GRAY = '#0d9488', '#71717a'
left_center = pie.point(-5, 0.5, out='left_center', hidden=True)
right_center = pie.point(5, 0.5, out='right_center', hidden=True)
pie.ellipse(left_center, 1.8, 3.8, 0, out='space_in', stroke=TEAL)
pie.ellipse(right_center, 1.8, 3.8, 0, out='space_out', stroke=TEAL)
pie.text('Rⁿ', -5.5, -4.1, 20, out='lab_rn', stroke=TEAL)
pie.text('Rᵐ', 4.5, -4.1, 20, out='lab_rm', stroke=TEAL)
f_arrow_a = pie.point(-3.2, 2.6, out='f_arrow_a', hidden=True)
f_arrow_b = pie.point(3.2, 2.6, out='f_arrow_b', hidden=True)
pie.annotate_arrow(f_arrow_a, f_arrow_b, 0, out='arrow_f', stroke=GRAY)
pie.text('x', -5.7, 2.9, 18, out='lab_x', stroke=GRAY)
pie.text('y', 5.3, 2.9, 18, out='lab_y', stroke=GRAY)
lab_f = pie.text('f', -0.2, 3.4, 18, out='lab_f', stroke=GRAY)

First, the original function fff carries a point XXX in RnR^nRn across to its image yyy in RmR^mRm (the grey layer).

lf_arrow_a = pie.point(-3.2, -1.6, out='lf_arrow_a', hidden=True)
lf_arrow_b = pie.point(3.2, -1.6, out='lf_arrow_b', hidden=True)
pie.annotate_arrow(lf_arrow_a, lf_arrow_b, 0, out='arrow_lf', stroke=TEAL)
induce_a = pie.point(0, 1.5, out='induce_a', hidden=True)
induce_b = pie.point(0, -0.5, out='induce_b', hidden=True)
pie.annotate_arrow(induce_a, induce_b, 0, out='arrow_induce', stroke=GRAY)
pie.text('dx', -6.1, -1.3, 18, out='lab_dx')
pie.text('dy', 5.3, -1.3, 18, out='lab_dy')
lab_lf = pie.text('Lf', -0.5, -2.1, 18, out='lab_lf', stroke=TEAL)

Then the linear operator LfL_fLf​ sends the small change dXdXdX to the corresponding change dYdYdY (the teal layer), the map that fff induces at the point XXX.

f(X)=X2Lf[dX]=X(dX)+(dX)X\begin{aligned} f(X) &= X^2 \\ L_f[dX] &= X(dX) + (dX)X \end{aligned}f(X)Lf​[dX]​=X2=X(dX)+(dX)X​

Verification using Python

import torch

def square(X):
  return X @ X

def d_square(dX):
  def operator(X):
    return X @ dX + dX @ X
  return operator

X = torch.randn(1000,1000,dtype=torch.float64)
dX = torch.randn_like(X).mul(1e-8)

actual_dy = (X+dX)@(X+dX) - X@X
# The below line is the same as
# dy = X @ dX + dX @ X
dy = d_square(dX)(X)

torch.allclose(actual_dy, dy)

'''
Outputs:
True
'''

Note: Lf[dX]L_f[dX]Lf​[dX] is linear in dXdXdX but may not be linear in XXX. For example:

f(X)=X3Lf[dX]=f(X+dX)−f(X)=(X+dX)3−X3=(dX)X2+X(dX)X+X2(dX)\begin{aligned} f(X) &= X^3 \\ L_f[dX] &= f(X+dX) - f(X) \\ &= (\textcolor{#1d4ed8}{X}+\textcolor{#be185d}{dX})^3 - \textcolor{#1d4ed8}{X^3} \\ &= \textcolor{#b45309}{(dX)X^2 + X(dX)X + X^2(dX)} \end{aligned}f(X)Lf​[dX]​=X3=f(X+dX)−f(X)=(X+dX)3−X3=(dX)X2+X(dX)X+X2(dX)​

which is not linear in XXX.


1. Product rule and chain rule

Consider two (possibly vector-valued) functions along with their (differential) linear operators:

f(x+dx)=f(x)+Lf[dx]g(x+dx)=g(x)+Lg[dx]\begin{aligned} f(x+dx) &= f(x) + L_f[dx] \\ g(x+dx) &= g(x) + L_g[dx] \end{aligned}f(x+dx)g(x+dx)​=f(x)+Lf​[dx]=g(x)+Lg​[dx]​

Then for a function h(x)=f(x)∗g(x)h(x) = f(x) * g(x)h(x)=f(x)∗g(x), we have:

dh=h(x+dx)−h(x)=(f(x)+Lf[dx])∗(g(x)+Lg[dx])−f(x)∗g(x)=Lf[dx]∗g(x)+f(x)∗Lg[dx]+Lf[dx]∗Lg[dx]⏟higher order=Lf[dx]∗g(x)+f(x)∗Lg[dx]\begin{aligned} dh &= h(x+dx) - h(x) \\ &= \big(f(x) + \textcolor{#be185d}{L_f[dx]}\big) * \big(g(x) + \textcolor{#047857}{L_g[dx]}\big) - f(x)*g(x) \\ &= \textcolor{#be185d}{L_f[dx]} * g(x) + f(x) * \textcolor{#047857}{L_g[dx]} + \underbrace{\textcolor{#be185d}{L_f[dx]} * \textcolor{#047857}{L_g[dx]}}_{\text{higher order}} \\ &= \textcolor{#b45309}{L_f[dx] * g(x) + f(x) * L_g[dx]} \end{aligned}dh​=h(x+dx)−h(x)=(f(x)+Lf​[dx])∗(g(x)+Lg​[dx])−f(x)∗g(x)=Lf​[dx]∗g(x)+f(x)∗Lg​[dx]+higher orderLf​[dx]∗Lg​[dx]​​=Lf​[dx]∗g(x)+f(x)∗Lg​[dx]​

This is the product rule for vector valued functions (the last term is dropped as a higher order term).

Now consider another function p(x)=f(g(x))p(x) = f(g(x))p(x)=f(g(x)). Then we have:

dp=p(x+dx)−p(x)=f(g(x+dx))−f(g(x))=f(g(x)+Lg[dx]⏟dg)−f(g(x))=Lf[dg]=Lf[Lg[dx]]\begin{aligned} dp &= p(x+dx) - p(x) \\ &= f(g(x+dx)) - f(g(x)) \\ &= f(g(x) + \underbrace{\textcolor{#047857}{L_g[dx]}}_{\textcolor{#047857}{dg}}) - f(g(x)) \\ &= L_f[\textcolor{#047857}{dg}] \\ &= \textcolor{#b45309}{L_f[\textcolor{#047857}{L_g[dx]}]} \end{aligned}dp​=p(x+dx)−p(x)=f(g(x+dx))−f(g(x))=f(g(x)+dgLg​[dx]​​)−f(g(x))=Lf​[dg]=Lf​[Lg​[dx]]​

This is the chain rule for vector valued functions. Note that Lg[dx]L_g[dx]Lg​[dx] is evaluated at xxx and Lf[Lg[dx]]L_f[L_g[dx]]Lf​[Lg​[dx]] is evaluated at g(x)g(x)g(x). This will become clear with an example below.


2. Some examples

Below are some interesting examples of differential linear operators. Note that d(AT)=(dA)Td(A^T) = (dA)^Td(AT)=(dA)T for any matrix or vector AAA.


2.1. f(x)=∥x∥2f(x) = \lVert x \rVert^2f(x)=∥x∥2 where xxx is a vector

This function calculates the squared norm of a vector. It can be re-written as f(x)=∥x∥2=xTxf(x) = \lVert x \rVert^2 = x^Txf(x)=∥x∥2=xTx. Now we just use dfdfdf instead of LfL_fLf​:

df=f(x+dx)−f(x)=(x+dx)T(x+dx)−xTx=xTx+(dx)Tx+xT(dx)+(dx)T(dx)⏟higher order−xTx=(dx)Tx+xT(dx)=xT(dx)+xT(dx)=2xTdx\begin{aligned} df &= f(x+dx) - f(x) \\ &= (\textcolor{#1d4ed8}{x}+\textcolor{#be185d}{dx})^T(\textcolor{#1d4ed8}{x}+\textcolor{#be185d}{dx}) - \textcolor{#1d4ed8}{x^Tx} \\ &= \textcolor{#1d4ed8}{x^Tx} + \textcolor{#be185d}{(dx)}^T\textcolor{#1d4ed8}{x} + \textcolor{#1d4ed8}{x}^T\textcolor{#be185d}{(dx)} + \underbrace{\textcolor{#be185d}{(dx)}^T\textcolor{#be185d}{(dx)}}_{\text{higher order}} - \textcolor{#1d4ed8}{x^Tx} \\ &= \textcolor{#be185d}{(dx)}^T\textcolor{#1d4ed8}{x} + \textcolor{#1d4ed8}{x}^T\textcolor{#be185d}{(dx)} \\ &= \textcolor{#1d4ed8}{x}^T\textcolor{#be185d}{(dx)} + \textcolor{#1d4ed8}{x}^T\textcolor{#be185d}{(dx)} \\ &= \textcolor{#b45309}{2x^Tdx} \end{aligned}df​=f(x+dx)−f(x)=(x+dx)T(x+dx)−xTx=xTx+(dx)Tx+xT(dx)+higher order(dx)T(dx)​​−xTx=(dx)Tx+xT(dx)=xT(dx)+xT(dx)=2xTdx​

where we used the fact that (dx)Tx(dx)^Tx(dx)Tx is a scalar and hence equals xTdxx^TdxxTdx. Thus, if the change dxdxdx is orthogonal to the vector xxx, then the change in output dydydy is zero since xTdx=0x^Tdx = 0xTdx=0. This makes sense because this small change can be seen as a rotation of the vector xxx. The rotation does not change the length of xxx.


2.2. f(X)=Xnf(X) = X^nf(X)=Xn

df=∑i=0n−1Xi(dX)Xn−1−idf = \sum_{i=0}^{n-1} X^i(dX)X^{n-1-i}df=i=0∑n−1​Xi(dX)Xn−1−i

We already saw this for the case of f(X)=X2f(X) = X^2f(X)=X2. To prove this, one can apply the product rule and use induction with the initial condition df(X)=dXdf(X) = dXdf(X)=dX. Assuming that it is true for f(X)=Xn−1f(X) = X^{n-1}f(X)=Xn−1, we have:

df=d(XXn−1)=(dX)Xn−1+X d(Xn−1)=(dX)Xn−1+X∑i=0n−2Xi(dX)Xn−2−i=(dX)Xn−1⏟i=0 term+∑i=1n−1Xi(dX)Xn−1−i⏟shift i → i+1=∑i=0n−1Xi(dX)Xn−1−i\begin{aligned} df &= d(XX^{n-1}) \\ &= (dX)X^{n-1} + X\,d(X^{n-1}) \\ &= (dX)X^{n-1} + X\sum_{i=0}^{n-2} X^i(dX)X^{n-2-i} \\ &= \underbrace{(dX)X^{n-1}}_{i=0 \text{ term}} + \underbrace{\sum_{i=1}^{n-1} X^i(dX)X^{n-1-i}}_{\text{shift } i \,\to\, i+1} \\ &= \textcolor{#b45309}{\sum_{i=0}^{n-1} X^i(dX)X^{n-1-i}} \end{aligned}df​=d(XXn−1)=(dX)Xn−1+Xd(Xn−1)=(dX)Xn−1+Xi=0∑n−2​Xi(dX)Xn−2−i=i=0 term(dX)Xn−1​​+shift i→i+1i=1∑n−1​Xi(dX)Xn−1−i​​=i=0∑n−1​Xi(dX)Xn−1−i​

Note how it is similar to the case of d(xn)=nxn−1d(x^n) = nx^{n-1}d(xn)=nxn−1. However since matrix multiplication is not commutative, the order of dXdXdX and XXX is important.


2.3. f(X)=X−1f(X) = X^{-1}f(X)=X−1

One can use the identity XX−1=IXX^{-1} = IXX−1=I and then apply the product rule to it:

d(XX−1)=d(I)(dX)X−1⏟d(X) X−1+X(dX−1)⏟X d(X−1)=0X(dX−1)=−(dX)X−1dX−1=−X−1(dX)X−1\begin{aligned} d(XX^{-1}) &= d(I) \\ \underbrace{(dX)X^{-1}}_{d(X)\,X^{-1}} + \underbrace{X(dX^{-1})}_{X\,d(X^{-1})} &= 0 \\ X(dX^{-1}) &= -(dX)X^{-1} \\ dX^{-1} &= \textcolor{#b45309}{-X^{-1}(dX)X^{-1}} \end{aligned}d(XX−1)d(X)X−1(dX)X−1​​+Xd(X−1)X(dX−1)​​X(dX−1)dX−1​=d(I)=0=−(dX)X−1=−X−1(dX)X−1​

where the last line follows from left-multiplying both sides by X−1X^{-1}X−1.


2.4. f(Q)=QQTf(Q) = QQ^Tf(Q)=QQT where QQQ is an orthonormal matrix

Using the property of orthonormal matrix, we have QTQ=IQ^TQ = IQTQ=I. We can use the fact that d(QTQ)=d(I)=0d(Q^TQ) = d(I) = 0d(QTQ)=d(I)=0.

d(QTQ)=d(I)(dQ)TQ⏟= (QTdQ)T+QTdQ=0QTdQ=−(QTdQ)T\begin{aligned} d(Q^TQ) &= d(I) \\ \underbrace{(dQ)^TQ}_{= \,(Q^TdQ)^T} + Q^TdQ &= 0 \\ Q^TdQ &= -(Q^TdQ)^T \end{aligned}d(QTQ)=(QTdQ)T(dQ)TQ​​+QTdQQTdQ​=d(I)=0=−(QTdQ)T​

Thus QTdQQ^TdQQTdQ is negative of its transpose, which means QTdQQ^TdQQTdQ is a skew-symmetric matrix. This insight is a motivation for key ideas behind Lie algebra, a quick introduction of which can be found here.


3. Closing example

We end this part with an example of a chain rule for a matrix-valued function. We have:

g(X)=X2f(X)=X−1h(X)=f(g(X))dh=Lf[Lg[dX]]=Lf[X(dX)+(dX)X⏟dg]=−(X2)−1(X(dX)+(dX)X)(X2)−1\begin{aligned} g(X) &= X^2 \\ f(X) &= X^{-1} \\ h(X) &= f(g(X)) \\ dh &= L_f[\textcolor{#047857}{L_g[dX]}] \\ &= L_f[\textcolor{#047857}{\underbrace{X(dX) + (dX)X}_{dg}}] \\ &= \textcolor{#b45309}{-(X^2)^{-1}\big(X(dX) + (dX)X\big)(X^2)^{-1}} \end{aligned}g(X)f(X)h(X)dh​=X2=X−1=f(g(X))=Lf​[Lg​[dX]]=Lf​[dgX(dX)+(dX)X​​]=−(X2)−1(X(dX)+(dX)X)(X2)−1​

This can be numerically verified like so:

import torch
x = torch.rand(1000,1000).double()
dx = torch.randn_like(x).mul(1e-8)

def square(x):
  return x @ x

def invert(x):
  return torch.linalg.inv(x)

def d_square(dx):
  def operator(x):
    return x @ dx + dx @ x
  return operator

def d_invert(dx):
  def operator(x):
    x_inv = torch.linalg.inv(x)
    return -x_inv @ dx @ x_inv
  return operator

Finally, we perturb the input by a small value and record the change in the output dYdYdY. Finally we calculate the change in output using the chain rule dfdfdf.

y = invert(square(x))
dy = invert(square(x+dx)) - y

g = square(x)
dg = d_square(dx)(x)
df = d_invert(dg)(g)
torch.allclose(df, dy)

'''
Outputs:
True
'''

Gradient of a function →