In Chapter 1.4, we learned how to add and subtract vectors and how to perform scalar multiplication. Scalar multiplication lets us stretch or compress a vector by multiplying it by a scalar; a negative scalar also reverses its direction. Each of these operations produces another vector. In Chapter 1.5, we learned how to find vector lengths and distances between points.
But what about multiplying a vector by another vector? We haven’t defined that yet! In this section, we’ll introduce one way to do it: the dot product.
The dot product will also help us answer a geometric question: what is the angle between two vectors? In particular, how can we tell whether two vectors are perpendicular without relying on a picture?
Multiply corresponding components, then add the products. For example, if
u=[21],v=[13],
then
u⋅v=2(1)+1(3)=5.
The result is one number, not another vector. We’ll call this the algebraic definition, since it tells us how to calculate a dot product from the components, using algebra.
The first property says that the order doesn’t matter. The second lets us distribute over addition. The third lets us pull scalars outside the dot product. Remember that a dot product produces a scalar: (u⋅v)⋅w is not a valid operation, since its first input is a scalar rather than a vector.
In Chapter 1.5, we found the length of a linear combination by computing its components first. Here, the dot product properties let us calculate directly from the information we’re given!
import numpy as np
import plotly.graph_objects as go
from pathlib import Path as _PlotPath
import sys as _plot_sys
_notes_root = next(path for path in [_PlotPath.cwd(), *_PlotPath.cwd().parents] if (path / "myst.yml").exists())
if str(_notes_root) not in _plot_sys.path:
_plot_sys.path.insert(0, str(_notes_root))
from plot_style import style_plotly as _style_plotly
def _vector_figure(vectors, vdeltax=0.3, vdeltay=0.3, vdeltaz=0.3):
dimension = len(vectors[0][0][0])
fig = go.Figure()
for (start, end), color, label in vectors:
start = np.asarray(start, dtype=float)
end = np.asarray(end, dtype=float)
delta = end - start
if dimension == 2:
fig.add_trace(go.Scatter(
x=[start[0], end[0]], y=[start[1], end[1]], mode="lines",
line=dict(color=color, width=4), showlegend=False,
hovertemplate="(%{x}, %{y})<extra></extra>",
))
fig.add_annotation(
x=end[0], y=end[1], ax=start[0], ay=start[1],
xref="x", yref="y", axref="x", ayref="y",
showarrow=True, arrowhead=3, arrowsize=1.2, arrowwidth=3,
arrowcolor=color,
)
if label:
midpoint = (start + end) / 2
fig.add_annotation(
x=midpoint[0] + vdeltax, y=midpoint[1] + vdeltay,
text=label, showarrow=False, font=dict(color=color, size=16),
)
else:
fig.add_trace(go.Scatter3d(
x=[start[0], end[0]], y=[start[1], end[1]], z=[start[2], end[2]],
mode="lines", line=dict(color=color, width=8),
showlegend=False, hoverinfo="skip",
))
length = np.linalg.norm(delta)
if length:
unit = delta / length
fig.add_trace(go.Cone(
x=[end[0]], y=[end[1]], z=[end[2]],
u=[unit[0]], v=[unit[1]], w=[unit[2]],
anchor="tip", sizemode="absolute", sizeref=0.28,
colorscale=[[0, color], [1, color]], showscale=False,
hoverinfo="skip",
))
if label:
midpoint = (start + end) / 2
fig.add_trace(go.Scatter3d(
x=[midpoint[0] + vdeltax], y=[midpoint[1] + vdeltay],
z=[midpoint[2] + vdeltaz], mode="text", text=[label],
textfont=dict(color=color, size=16), hoverinfo="skip",
showlegend=False,
))
return fig
def plot_vectors_non_origin(vectors, **kwargs):
return _vector_figure(vectors, **kwargs)
def plot_vectors(vectors, **kwargs):
converted = []
for endpoint, color, label in vectors:
converted.append(((tuple(0 for _ in endpoint), endpoint), color, label))
return _vector_figure(converted, **kwargs)
def _set_vector_colors(fig, vectors, overrides):
if np.asarray(vectors[0][0]).ndim == 1:
vectors = [((tuple(0 for _ in endpoint), endpoint), color, label) for endpoint, color, label in vectors]
dimension = len(vectors[0][0][0])
trace_index = 0
annotation_index = 0
for vector_index, ((start, end), _, label) in enumerate(vectors):
color = overrides.get(vector_index)
if dimension == 2:
if color:
fig.data[trace_index].line.color = color
fig.layout.annotations[annotation_index].arrowcolor = color
if label:
fig.layout.annotations[annotation_index + 1].font.color = color
trace_index += 1
annotation_index += 1 + bool(label)
else:
if color:
fig.data[trace_index].line.color = color
trace_index += 1
if np.linalg.norm(np.asarray(end) - np.asarray(start)):
if color:
fig.data[trace_index].colorscale = [[0, color], [1, color]]
trace_index += 1
if label:
if color:
fig.data[trace_index].textfont.color = color
trace_index += 1
return fig
def finish_vectors(fig, vectors, renderer="png"):
_style_plotly(fig, renderer=renderer)
_set_vector_colors(fig, vectors, {i: item[1] for i, item in enumerate(vectors)})
return fig
In R2 or R3, we can draw two nonzero vectors from the same starting point and look at the angle between their arrows. The dot product has a second, geometric definition that relates it to this angle.
Why is this formula useful? We don’t typically know the angle between two vectors beforehand; we usually just have the vectors and their components. But, since the two definitions of the dot product are equal, we can compute the dot product and lengths from the components, then rearrange the geometric formula to solve for the angle. This gives us a way to measure how close the vectors are to pointing in the same direction, which has many useful applications.
In two and three dimensions, this gives the familiar geometric angle. What does an angle in R4 or R5 look like? We can’t directly picture those spaces, but we can still calculate the ratio: in higher dimensions, take this as the definition of the angle, choosing the unique angle between 0∘ and 180∘ with this cosine. The ratio always lies between -1 and 1.
If we’re asked for the cosine of the angle, or the cosine similarity, the ratio
∥u∥∥v∥u⋅v
is our answer. To find the angle itself, take cos−1 of the result. Here cos−1 means inverse cosine, not 1/cos. Let’s work through both steps.
This is an exact answer. If we wanted the angle in degrees, we could use a calculator in degree mode:
θ=cos−1(418637)≈51.5∘.
Draw both vectors from the origin. The shading below shows part of the plane containing them, and the black arc marks their angle. Drag the figure to rotate it. The angle may look different from different viewpoints, but its value stays the same.
Since both lengths are positive, the dot product and cosine similarity have the same sign. We can interpret that sign geometrically:
Dot product
Angle between nonzero vectors
Positive
0∘≤θ<90∘: the directions are less than a right angle apart.
Zero
θ=90∘: the vectors are perpendicular.
Negative
90∘<θ≤180∘: the directions are more than a right angle apart.
A cosine similarity of 1 means the vectors point in the same direction; -1 means they point in opposite directions. The closer it is to 1, the smaller the angle between them.
Let’s keep u=[21] fixed and compare it with four different vectors.
from plotly.subplots import make_subplots
test_vectors = [(1,3), (3,3), (-1,2), (-2,-1)]
titles = [
rf'$\vec u\cdot\vec v = \begin{{bmatrix}}2\\1\end{{bmatrix}}\cdot\begin{{bmatrix}}{a}\\{b}\end{{bmatrix}} = {2*a+b}$'
for a, b in test_vectors
]
fig = make_subplots(rows=2, cols=2, subplot_titles=titles, horizontal_spacing=0.12, vertical_spacing=0.23)
_style_plotly(fig, renderer='png')
for i, endpoint in enumerate(test_vectors):
row, col = divmod(i, 2)
row, col = row+1, col+1
axis_num = i+1
xref = 'x' if axis_num == 1 else f'x{axis_num}'
yref = 'y' if axis_num == 1 else f'y{axis_num}'
for point, color, label in [((2,1),'orange',r'$\vec u$'),(endpoint,'#3d81f6',r'$\vec v$')]:
fig.add_trace(go.Scatter(x=[0,point[0]],y=[0,point[1]],mode='lines',
line=dict(color=color,width=3),showlegend=False),row=row,col=col)
fig.add_annotation(x=point[0],y=point[1],ax=0,ay=0,xref=xref,yref=yref,
axref=xref,ayref=yref,showarrow=True,arrowhead=3,arrowwidth=3,arrowcolor=color)
fig.add_annotation(x=point[0]+0.25,y=point[1]+0.25,xref=xref,yref=yref,
text=label,showarrow=False,font=dict(color=color,size=17))
if i == 3:
fig.layout.annotations[-1].update(x=-2.25, y=-0.45)
fig.update_xaxes(range=[-3,4],dtick=1,constrain='domain',row=row,col=col)
fig.update_yaxes(range=[-2,4],dtick=1,scaleanchor=xref,scaleratio=1,row=row,col=col)
fig.update_layout(width=800,height=740,margin=dict(l=35,r=25,t=85,b=35))
fig.show(scale=2)
The first two dot products are positive, the third is zero, and the fourth is negative. In the fourth picture, the vectors point in exactly opposite directions, so the angle is 180∘ and their cosine similarity is -1.
Be careful when comparing the sizes of dot products: they depend on the lengths as well as the angle. Doubling one vector doubles the dot product without changing its direction. Cosine similarity removes this dependence on length: its numerator and denominator both double, so the ratio stays the same. More generally, multiplying either vector by a positive scalar leaves their cosine similarity unchanged.
This connects directly to unit vectors, which describe direction. The cosine similarity is the dot product of the two vectors after normalizing them:
∥u∥u⋅∥v∥v=∥u∥∥v∥u⋅v.
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from pathlib import Path
proof_u = np.array([3., 1.])
proof_v = np.array([4., -6.])
proof_colors = ['orange', '#3d81f6', '#d81a60']
proof_fig = make_subplots(rows=1, cols=2, horizontal_spacing=0.18,
subplot_titles=['Vectors', 'Side lengths'])
proof_sides = [(np.zeros(2), proof_u), (np.zeros(2), proof_v), (proof_v, proof_u)]
proof_labels = [
[r'$\vec u$', r'$\vec v$', r'$\vec u - \vec v$'],
[r'$\lVert\vec u\rVert$', r'$\lVert\vec v\rVert$', r'$\lVert\vec u - \vec v\rVert$']
]
proof_label_positions = [(1.35, 1.1), (1.3, -3.4), (4.55, -2.1)]
for panel in [1, 2]:
xref = 'x' if panel == 1 else 'x2'
yref = 'y' if panel == 1 else 'y2'
for i, ((start, end), color) in enumerate(zip(proof_sides, proof_colors)):
proof_fig.add_trace(go.Scatter(
x=[start[0], end[0]], y=[start[1], end[1]], mode='lines',
line=dict(color=color, width=3), showlegend=False, hoverinfo='skip'), row=1, col=panel)
if panel == 1:
proof_fig.add_annotation(x=end[0], y=end[1], ax=start[0], ay=start[1],
xref=xref, yref=yref, axref=xref, ayref=yref, text='',
showarrow=True, arrowhead=3, arrowsize=1.1, arrowwidth=3, arrowcolor=color)
x, y = proof_label_positions[i]
if i == 2:
# Separate annotations keep the subtraction sign black in PNG exports.
pieces = [(4.25, r'$\vec u$', color), (4.65, '−', 'black'), (5.05, r'$\vec v$', color)]
if panel == 2:
pieces = [(4.25, r'$\lVert\vec u$', color), (4.8, '−', 'black'), (5.35, r'$\vec v\rVert$', color)]
for label_x, label_text, label_color in pieces:
proof_fig.add_annotation(x=label_x, y=y, xref=xref, yref=yref,
text=label_text, showarrow=False, font=dict(color=label_color, size=18))
else:
proof_fig.add_annotation(x=x, y=y, xref=xref, yref=yref,
text=proof_labels[panel-1][i], showarrow=False, font=dict(color=color, size=18))
angles=np.linspace(np.arctan2(proof_v[1], proof_v[0]), np.arctan2(proof_u[1], proof_u[0]), 80)
proof_fig.add_trace(go.Scatter(x=0.65*np.cos(angles), y=0.65*np.sin(angles),
mode='lines', line=dict(color='black',width=1.5), showlegend=False, hoverinfo='skip'),row=1,col=panel)
proof_fig.add_annotation(x=0.95,y=-0.35,xref=xref,yref=yref,text=r'$\theta$',
showarrow=False,font=dict(color='black',size=19))
proof_fig.update_xaxes(range=[-0.6,6.3],visible=False,constrain='domain',row=1,col=panel)
proof_fig.update_yaxes(range=[-6.8,2.0],visible=False,scaleanchor=xref,scaleratio=1,row=1,col=panel)
proof_fig.update_layout(width=760,height=440,margin=dict(l=15,r=15,t=50,b=10),
paper_bgcolor='white',plot_bgcolor='white',font=dict(family='Palatino',size=18,color='black'))
Path('imgs').mkdir(exist_ok=True)
proof_fig.write_image('imgs/dot-product-proof.png',scale=2)
proof_fig.show(renderer='png',scale=2)
Optional proof: Why the two formulas agree
Let’s consider two nonzero vectors, u and v, in R2 or R3. Draw them from the same starting point. In three dimensions, two vectors that do not point along the same line lie in a plane, so we can still use ordinary triangle geometry.
Along with them, let’s consider their difference, u−v. This step may seem arbitrary, but we’ll see why it’s useful soon. The diagram shows the same triangle twice: first with its vectors, then with its side lengths. We haven’t put numbers on the vectors, since the proof should work in general.
Which way should the difference arrow point? Start by walking along v, then walk along u−v. You end up at the tip of u, just as we’d expect from
v+(u−v)=u.
So the magenta arrow points from the tip of v to the tip of u. Its length, ∥u−v∥, is the distance between the two tips, as we learned in Chapter 1.5. We’ll find two expressions for the square of this distance.
First, use the law of cosines. A triangle with sides of lengths a and b enclosing an angle θ has opposite side length c satisfying
c2=a2+b2−2abcosθ.
In our triangle, a=∥u∥, b=∥v∥, and c=∥u−v∥. Therefore,
∥u−v∥2=∥u∥2+∥v∥2−2∥u∥∥v∥cosθ.
Second, use the computational formula. Taking the dot product of a vector with itself gives its squared length, so
In the last step, we combined the two middle terms because u⋅v=v⋅u.
Let’s take a step back. We’ve found two expressions for the same squared distance, so they must be equal:
∥u∥2+∥v∥2−2∥u∥∥v∥cosθ=∥u∥2+∥v∥2−2u⋅v.
Subtracting the two squared-length terms from both sides gives
−2∥u∥∥v∥cosθ=−2u⋅v.
Finally, divide both sides by -2:
u⋅v=∥u∥∥v∥cosθ.
This proves that the two formulas agree! If the vectors point along the same line, the triangle is flat. The same law-of-cosines identity still holds with θ=0∘ or 180∘.
Orthogonal is the word we use for perpendicular vectors. The geometric formula gives us an especially useful test: for nonzero vectors, the angle is 90∘ exactly when the dot product is zero, because cos90∘=0.
This definition applies to any two vectors with the same number of components, no matter how many components they have. For example, two vectors u,v∈R15 are orthogonal exactly when u⋅v=0. We don’t need to picture 15-dimensional space to check this: orthogonal means their dot product is zero.
For example,
[21]⋅[−12]=2(−1)+1(2)=0,
so these vectors are orthogonal. The picture below shows these two perpendicular vectors.
In Chapter 1.5, we used the Pythagorean theorem to define the length of a vector. Here, we’ve already learned that the dot product operation and the length of a vector are closely related: for any vector x,
x⋅x=∥x∥2.
What happens if we apply this equality to the sum of two vectors, u+v? Using the distributive property of the dot product,
If u and v are orthogonal, the middle term is zero, leaving
∥u+v∥2=∥u∥2+∥v∥2.
This is the Pythagorean theorem! For nonzero vectors in two or three dimensions, draw them tip-to-tail: they form the perpendicular legs of a right triangle, and their sum is the hypotenuse. The algebra also works in higher dimensions. It tells us exactly when we can add the squared lengths: when the two vectors are orthogonal.
How large can a dot product be? The answer depends on the lengths of the two vectors.
This says that the absolute value of the dot product is at most the product of the vectors’ lengths. For two nonzero vectors, both lengths are positive, so we can divide by their product to get
∣∣∥u∥∥v∥u⋅v∣∣≤1.
The quantity inside the absolute value is the cosine similarity of u and v. So the cosine similarity has absolute value at most 1. Remember that ∣x∣≤1 means −1≤x≤1. This is reassuring: the cosine similarity is the cosine of the angle between the vectors, and the cosine function only takes values between -1 and 1, inclusive.
We can also rewrite the original inequality without absolute values:
−∥u∥∥v∥≤u⋅v≤∥u∥∥v∥.
For nonzero vectors, we can interpret these bounds using the geometric formula. The upper bound occurs when the vectors point in the same direction, and the lower bound occurs when they point in opposite directions. If either vector is zero, the dot product and both bounds are zero.
For example, if ∥u∥=5 and ∥v∥=12, their dot product must be between -60 and 60. It is 60 when they point in the same direction, and -60 when they point in opposite directions.
Homework 2 will have questions that develop this idea further.
Suppose you pull an object along a straight track. A force pointing in the direction of the displacement does positive work; a perpendicular force does no work.
For a constant forceF acting while an object undergoes displacement d, the work done by that force is
W=F⋅d.
If both vectors are nonzero, the geometric formula gives the familiar formula from physics,
W=∥F∥∥d∥cosθ,
where θ is the angle between the force and displacement. If either vector is zero, the dot product tells us directly that the work is zero.
If force is measured in newtons and displacement in meters, work is measured in joules.
For example, suppose F=[34] newtons and the object moves d=[20] meters along a horizontal track. Then
W=3(2)+4(0)=6 joules.
The vertical component of this force contributes no work because there is no vertical displacement.