The next operation after addition and scalar multiplication is matrix multiplication: how do we multiply matrices to form a product like AB? We’ll get there eventually, but first let’s consider a simpler task: multiplying a matrix A by a vector v.
What did we just do? There are two interpretations.
Interpretation 1: Dot products of v with each row of A¶
The operations that led us to 26 and 11 look like dot products. Indeed, those values came from dot products, but only if we treat each row as a column vector; you can’t take a dot product between a row vector and a column vector. The two dot products we computed were
To take these dot products, we needed the rows of A to have the same number of entries as v. Each row of A had 3 entries, because A had 3 columns. This tells us our golden rule:
There are d entries in each row, so the input needs d entries. There are n rows, so the output has n entries. Our first example matrix A takes a vector in R3 (⎣⎡2−13⎦⎤) and turns it into a vector in R2 ([2611]).
Interpretation 2: Linear combination of the columns of A¶
So Av is a linear combination of the columns of A! This is an incredibly important interpretation of the operation. The entries of v are the coefficients.
Click through the figure below to see both interpretations of the same calculation.
import html
import warnings
from pathlib import Path
from IPython.display import HTML, display
_notes_root = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / 'myst.yml').exists())
_widget = (_notes_root / 'ch03/multiplication-widget.html').read_text()
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message='Consider using IPython.display.IFrame instead')
display(HTML('<iframe title="Scroll through a matrix-vector product: rows and columns" style="display:block;width:100%;height:690px;border:1px solid #cbd5e1;border-radius:6px;background:white" srcdoc="' + html.escape(_widget, quote=True) + '"></iframe>'))
Av is different from vA. In fact, the latter is usually not defined! If A has shape n×d and v has shape d×1, then
n×dAd×1v=n×1Av.
Reversing the order gives
d×1vn×dA.
Now the inner numbers are 1 and n, so this product is only defined when n=1. For our first example, Av has shapes (2×3)(3×1) and is defined, while vA has shapes (3×1)(2×3) and is not.
Even in the special case n=1, the two orders do different things: Av has shape 1×1, while vA has shape d×d.
When we multiply A by v, what comes out is a new vector, Av. We should think of the process of multiplying v by A as transformingv from one vector to another.
inputv⟶Multiply by A⟶outputAv.
Let’s try and understand the various ways a matrix A can transform a vector v. If A has shape n×d, the input has shape d×1 and the output has shape n×1. The input and output don’t even need to have the same number of entries!
For example, consider the 2×5 matrix
A=[102−102−1131].
Multiplication by A takes a vector in R5 and produces a vector in R2. Here is one input and its output:
The input has shape 5×1, and the output has shape 2×1.
For the next five examples, we’ll use 2×2 matrices. Then both the input and output have shape 2×1, so both are vectors in R2. This lets us draw the transformation on a coordinate plane and see what it does.
We’ll use the same two input vectors in each example:
u=[−46],v=[5−3].
In each pair of graphs, the inputs appear on the left and the outputs appear on the right, with the same axes and scale. Orange identifies u and Au; blue identifies v and Av. We’ll also multiply A by an arbitrary input [ab] to understand what happens to every vector.
import sys
import plotly.graph_objects as go
import plotly.io as pio
# Kaleido 1.0 does not accept Plotly 7's optional HTTP headers.
if hasattr(pio.defaults, 'headers'):
pio.defaults.headers = {}
from IPython.display import Image
if str(_notes_root) not in sys.path:
sys.path.insert(0, str(_notes_root))
from plot_style import style_plotly
def transformation_plot(points=None, names=None, limit=16):
fig = style_plotly(go.Figure())
colors = ['orange', '#3d81f6']
if points is not None:
for point, name, color in zip(points, names, colors):
fig.add_trace(go.Scatter(x=[0,point[0]], y=[0,point[1]], mode='lines', line=dict(color=color,width=3), showlegend=False))
fig.add_annotation(x=point[0],y=point[1],ax=0,ay=0,axref='x',ayref='y',showarrow=True,arrowhead=3,arrowsize=1.2,arrowwidth=2,arrowcolor=color,text='')
coords = r'\\'.join(str(x) for x in point)
label = '$'+name+r'=\begin{bmatrix}'+coords+r'\end{bmatrix}$'
fig.add_annotation(x=point[0],y=point[1],text=label,showarrow=False,xshift=(-50 if point[0] > 0.65*limit else (42 if point[0]>=0 else -45)),yshift=24,font=dict(color=color,size=18),bgcolor='rgba(255,255,255,0.85)')
fig.update_layout(width=520,height=500,margin=dict(l=42,r=20,t=25,b=42),font=dict(family='Palatino',size=16))
fig.update_xaxes(range=[-limit,limit],dtick=5 if limit==30 else 4,title='First coordinate',zeroline=True,zerolinewidth=1.5)
fig.update_yaxes(range=[-limit,limit],dtick=5 if limit==30 else 4,title='Second coordinate',scaleanchor='x',scaleratio=1,zeroline=True,zerolinewidth=1.5)
return fig
def save_plot(filename, points=None, names=None, limit=16):
png=transformation_plot(points,names,limit).to_image(format='png',scale=6)
(_notes_root/'ch03'/filename).write_bytes(png)
return png
save_plot('transformation-blank.png')
save_plot('transformation-blank-wide.png',limit=30)
save_plot('transformation-before.png',[[-4,6],[5,-3]],[r'\vec u',r'\vec v'])
_ = save_plot('transformation-before-wide.png',[[-4,6],[5,-3]],[r'\vec u',r'\vec v'],limit=30)
import base64
def display_pair(filename, points, names, limit=16):
after = save_plot(filename, points, names, limit)
before_name = 'transformation-before-wide.png' if limit == 30 else 'transformation-before.png'
before = (_notes_root / 'ch03' / before_name).read_bytes()
panels = []
for caption, png in [('Before: inputs', before), ('After: outputs', after)]:
encoded = base64.b64encode(png).decode('ascii')
panels.append('<figure style="margin:0;min-width:0;text-align:center">'
'<img alt="' + caption + '; orange represents u, blue represents v" '
'src="data:image/png;base64,' + encoded + '" '
'style="display:block;width:100%;height:auto;margin:0" />'
'<figcaption style="font:17px Palatino,serif;margin-top:6px">' + caption + '</figcaption></figure>')
display(HTML('<div style="display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px;width:100%;max-width:720px;margin:16px auto;background:white;color:black;padding:10px;box-sizing:border-box">' + ''.join(panels) + '</div>'))
The key here is that a diagonal matrix transforms each component of the input vector independently. This matrix multiplies the first component by −3/2 and the second by 2. Each output component depends only on the corresponding input component; the components do not get mixed together.
Even though the input vectors u and vdo not lie on the same line through the origin, the output vectors Au and Avdo! Multiplication by this matrix sends them onto the same line. Every output is a multiple of [3−2].
There are two questions we will explore over the coming weeks.
Given A and v, what is b=Av? Compute the output and describe the operation that A applies to v. The five examples above illustrate some possible operations: leaving vectors unchanged, scaling coordinates, swapping coordinates, sending vectors onto a line, and rotating vectors.
Given A and b, can we find v such that Av=b? This asks us to work backward from a desired output.
The second question need not have an answer. For the matrix in Example 4,
A[ab]=(a+2b)[3−2].
The target [24−16] is reachable: the input [−46] produces it. But [24−15] is unreachable, because every output satisfies 2x+3y=0. No input can make Av leave that line.
Even when an input exists, it need not be unique: [80] also produces [24−16].
There are two ways to understand Av: dot products with the rows, written as column vectors, and a linear combination of the columns. They always give the same result. Let’s see both with a new example:
A=⎣⎡23−2−145⎦⎤,v=[3−2].
The shapes are (3×2)(2×1), so the output has shape 3×1.
The row interpretation computes the output one entry at a time. The column interpretation builds the output as a sum of vectors. Both describe the same operation!