Think of this section as a brief detour from the main storyline of the course. Here, I’ll cover how to describe lines, planes, and hyperplanes in Rn, and how they relate to our understanding of the span of a set of vectors from Chapter 2.4.
We’re very familiar with lines in R2. The line y=2x+3, in R2, is the set of all (x,y) coordinates that satisfy the equation y=2x+3.
The line y=2x+3 is in slope-intercept form, which more generally looks like y=w0+w1x. Sometimes, we may write lines in standard form, like 2x−y+3=0, or more generally, ax+by+c=0.
Let’s kick things up a notch and consider lines in R3. What is the equation of theline below?
from utils import plot_vectors
import numpy as np
import plotly.graph_objects as go
v = (2, -1, 3)
# Create the line spanned by v (all scalar multiples)
t = np.linspace(-2, 2, 100)
line_x = t * v[0]
line_y = t * v[1]
line_z = t * v[2]
fig = plot_vectors([((0, 0, 0), "#3d81f6", r"")], show_axis_labels=True, vdeltax=0.1)
# Add the line spanned by v to the 3D plot
fig.add_trace(
go.Scatter3d(
x=line_x,
y=line_y,
z=line_z,
mode="lines",
line=dict(color="rgba(0,77,64,0.6)", width=5),
showlegend=False,
hoverinfo="skip"
)
)
# Draw small points at (0, 0, 0) and the tip of v
fig.add_trace(
go.Scatter3d(
x=[0, v[0]],
y=[0, v[1]],
z=[0, v[2]],
mode="markers",
marker=dict(size=4, color=["#222", "#222"]),
showlegend=False,
hoverinfo="skip"
)
)
# Add annotations next to the two points: (0,0,0) and (2,-1,3)
fig.add_trace(
go.Scatter3d(
x=[0],
y=[0],
z=[0],
mode="text",
text=["(0, 0, 0)"],
textposition="top right",
showlegend=False,
hoverinfo="skip"
)
)
fig.add_trace(
go.Scatter3d(
x=[v[0]],
y=[v[1]],
z=[v[2]],
mode="text",
text=[f"({v[0]}, {v[1]}, {v[2]})"],
textposition="top center",
showlegend=False,
hoverinfo="skip"
)
)
fig.update_layout(
scene_camera=dict(
eye=dict(x=-1.2, y=2, z=1.2)
),
scene=dict(
xaxis=dict(range=[-4, 4]),
yaxis=dict(range=[-4, 4]),
zaxis=dict(range=[-6, 6])
),
)
fig.show()
Loading...
As we saw in Chapter 2.4, the line shown above is the span of the vector v=⎣⎡2−13⎦⎤. It passes through the origin, (0,0,0), and passes through the point (2,−1,3).
Ideally, we’d be able to express the line as a function
z=f(x,y)
as we did in the R2 case, where y=f(x)=mx+b.
Unfortunately, there is no way to express the lines in R3, or R4, or Rn for n>2 as a simple function. Why not? If there were some formula for z (the height of the line) in terms of x and y, that would imply that we should be able to plug in anyx and anyy to get an output z. But, the line above only works for very specific combinations of x and y. For instance, there’s no point on the line above that has x=1 and y=1. Rather, when x=1, y is forced to be −21, and z is forced to be 23.
The key idea that I stressed in Chapter 2.4 is that lines are 1-dimensional objects, meaning that the location of any point on the line can be described using a single free variable.
So, the equation of the line above is
L=t⎣⎡2−13⎦⎤,t∈R
t here is a free variable – sometimes called a parameter (though this term is confusing in the context of our course) – meaning we can set it to whatever we’d like. The line is the set of all points that can be reached by plugging in different values of t.
Since the line is really a set of points, I should have written it as
L={t⎣⎡2−13⎦⎤∣t∈R}
but I’ll use the former notation for brevity.
Equivalently, you can think of the line as three separate functions of t. Pick a t. Then, L is
x=2ty=−tz=3t
Drag the value of t below to see how t allows us to move along the line.
from utils import plot_vectors
import numpy as np
import plotly.graph_objects as go
v = (2, -1, 3)
# Create the line spanned by v (all scalar multiples)
t = np.linspace(-2, 2, 25)
line_x = t * v[0]
line_y = t * v[1]
line_z = t * v[2]
fig = plot_vectors([((0, 0, 0), "#3d81f6", r"")], show_axis_labels=True, vdeltax=0.1)
# Add the line spanned by v to the 3D plot
fig.add_trace(
go.Scatter3d(
x=line_x,
y=line_y,
z=line_z,
mode="lines",
line=dict(color="rgba(0,77,64,0.6)", width=5),
showlegend=False,
hoverinfo="skip"
)
)
# Add a marker that moves along the line as t changes, controlled by a slider
t_slider = np.linspace(-2, 2, 25)
marker_x = t_slider * v[0]
marker_y = t_slider * v[1]
marker_z = t_slider * v[2]
# Create frames for the moving marker
frames = [
go.Frame(
data=[
go.Scatter3d(
x=[marker_x[i]],
y=[marker_y[i]],
z=[marker_z[i]],
mode="markers",
marker=dict(size=8, color="black"),
showlegend=False,
hoverinfo="skip"
)
],
name=str(round(t_slider[i], 2))
)
for i in range(len(t_slider))
]
# Find the index where t=0 to set as the default frame
default_t_index = np.argmin(np.abs(t_slider - 0))
# Add slider to control t, set default value to t=0
sliders = [
{
"steps": [
{
"method": "animate",
"args": [
[str(round(t_slider[i], 2))],
{"mode": "immediate", "frame": {"duration": 0, "redraw": True}, "transition": {"duration": 0}}
],
"label": f"t={round(t_slider[i],2)}"
}
for i in range(len(t_slider))
],
"currentvalue": {"prefix": ""},
"pad": {"t": 50},
"active": default_t_index
}
]
fig.frames = frames
fig.update_layout(
scene_camera=dict(
eye=dict(x=-1.2, y=2, z=1.2)
),
sliders=sliders,
scene=dict(
xaxis=dict(range=[-5, 5]),
yaxis=dict(range=[-5, 5]),
zaxis=dict(range=[-8, 8])
),
# Increase bottom margin to prevent axis ticks from being cut off
margin=dict(l=0, r=0, t=0, b=130),
width=650,
height=600
)
# Show the marker at t=0 by default
fig.add_trace(
go.Scatter3d(
x=[0],
y=[0],
z=[0],
mode="markers+text",
marker=dict(size=8, color="#aaa"),
showlegend=False,
hoverinfo="skip",
text=["at t = 0, line passes through (0, 0, 0)"],
textposition="top right",
textfont=dict(color="#333", size=12)
)
)
fig.show()
Loading...
The line L above passes through the origin, since if we set t=0, we get the point (0,0,0). This matches what we’d expect out of the span of a single vector, since 0v=0.
But how do we express a line that passes through some other fixed point that isn’t the origin? Such a line might not be the span of a single vector, since the span of a single vector is always a line that passes through the origin. But, it’s good to know how to think about lines in this more general form.
The definition above is not specific to 2-dimensional or 3-dimensional space – it works in any Rn. (Technically, I’m mixing the meaning of a point and a vector here, but as long as we remember that points describe positions and vectors describe directions, we should be fine.) Here’s a line in R100:
L=⎣⎡1234⋮100⎦⎤+t⎣⎡−1112−1314⋮110⎦⎤,t∈R
Note that the parametric form of a line is not unique! Since the parametric definition of a line depends on a “starting point” p0, we can pick any starting point we’d like. We can also scale the direction vector by any non-zero scalar. So,
L1=[12]+t[−34],t∈R
is the same line as
L2=[−26]+t[6−8],t∈R
once you consider all possible values of t in both cases. (I know this is a little confusing, since plugging the same value of t into L1 and into L2 will give you different points, but remember that L1 and L2 are sets, and so we need to consider all possible values of t.)
Below is a plot of L1=[12]+t[−34],t∈R.
from utils import plot_vectors_non_origin
p0 = (1, 2)
v = (-3, 4)
vectors = [
# ((start_coords, end_coords), color, label)
(([0, 0], p0), "orange", r"$\vec p_0 = \begin{bmatrix} 1 \\ 2 \end{bmatrix}$"),
((p0, tuple(np.array(p0) + np.array(v))), "#3d81f6", r"$\vec v = \begin{bmatrix} -3 \\ 4 \end{bmatrix}$"),
]
fig = plot_vectors_non_origin(vectors, show_axis_labels=True, vdeltax=1.2)
# Focus the plot on x in [-2, 4]
t = np.linspace(-2, 2, 100) # Solve for t so that x runs from -2 to 4
line_x = p0[0] + t * v[0]
line_y = p0[1] + t * v[1]
fig.add_trace(
go.Scatter(
x=line_x,
y=line_y,
mode="lines",
line=dict(color="rgba(0,77,64,0.6)", width=3, dash="dash"),
showlegend=False,
hoverinfo="skip",
zorder=0
),
)
# This annotation indicates the line represents all possible linear combinations of v alone
fig.add_annotation(
x=1.5 * v[0] + 7,
y=1.5 * v[1] - 5,
text="L₁",
showarrow=False,
font=dict(size=20, color="rgba(0,77,64,0.6)"),
bgcolor="rgba(255,255,255,0.8)"
)
fig.update_yaxes(
scaleanchor="x",
scaleratio=1,
range=[-2, 7]
)
fig.update_xaxes(
range=[-4, 4],
tickvals=np.arange(-6, 6, 1)
)
fig.show(renderer='png', scale=3)
And here’s L2=[−26]+t[6−8],t∈R.
import numpy as np
import plotly.graph_objects as go
from utils import plot_vectors_non_origin
p0 = (-2, 6)
v = (6, -8)
vectors = [
# ((start_coords, end_coords), color, label)
(([0, 0], p0), "orange", r"$\vec p_0 = \begin{bmatrix} -2 \\ 6 \end{bmatrix}$"),
((p0, tuple(np.array(p0) + np.array(v))), "#3d81f6", r"$\vec v = \begin{bmatrix} 6 \\ -8 \end{bmatrix}$"),
]
fig = plot_vectors_non_origin(vectors, show_axis_labels=True, vdeltax=1.2)
# Focus the plot on x in [-2, 4]
t = np.linspace(-2, 2, 100) # Solve for t so that x runs from -2 to 4
line_x = p0[0] + t * v[0]
line_y = p0[1] + t * v[1]
fig.add_trace(
go.Scatter(
x=line_x,
y=line_y,
mode="lines",
line=dict(color="rgba(0,77,64,0.6)", width=3, dash="dash"),
showlegend=False,
hoverinfo="skip",
zorder=0
),
)
# This annotation indicates the line represents all possible linear combinations of v alone
fig.add_annotation(
x=4.5,
y=1,
text="L₂ (same as the line above!)",
showarrow=False,
font=dict(size=20, color="rgba(0,77,64,0.6)"),
bgcolor="rgba(255,255,255,0.8)"
)
fig.update_yaxes(
scaleanchor="x",
scaleratio=1,
range=[-2, 7]
)
fig.update_xaxes(
range=[-4, 4],
tickvals=np.arange(-6, 6, 1)
)
fig.show(renderer='png', scale=3)
Note that we end up with the same line, despite the different starting points and direction vectors!
The proceeding activities give you some practice with the parametric form of a line.
Lines are 1-dimensional objects, whether they exist in R2, or R3, or R47, or in general Rn.
Similarly, planes are 2-dimensional objects. In R2, since there only exist two dimensions in the first place, the entirety of the coordinate system is one single plane, which we call the xy-plane.
Let’s start by building intuition for planes in R3, the most natural setting for them, and then generalize.
Note that both planes are flat surfaces that extend infinitely in all directions. The fact that the blue plane is cut off at the edges is just due to how I’m plotting the planes, not that there’s some boundary within which the plane is defined.
You’ll notice that the blue plane is relatively shallow, while the orange plane is relatively steep. Why?
I find the form z=Ax+By+C easier to understand intuitively, since it shows the rate of change of z with respect to x and y more clearly. Starting with z=Ax+By+C, we have that
∂x∂z=A,∂y∂z=B
In this example, the blue plane has A=53 and B=54, while the orange plane has A=−5 and B=−3, which explains their relative steepness.
That said, be careful, since a plane need not have a non-zero coefficient on z. For example, 3x+4y=0 and 3x+4y=5 is are perfectly valid planes, and they happen to be parallel.
draw_equations([[3, 4, 0, 0], [3, 4, 0, 5]])
Loading...
A key property of planes is that they are flat. Sure, we know that intuitively, but what does it actually mean?
This property is not true in general for other surfaces.
I first mentioned planes back in Chapter 2.1, when we intuitively discussed the fact that the set of all linear combinations of two non-collinear vectors in R3 forms a plane.
So, given two vectors u=⎣⎡u1u2u3⎦⎤,v=⎣⎡v1v2v3⎦⎤∈R3, how do we find the equation of the plane they span, in standard form?
If the plane they span is to be in standard form, it must be of the form ax+by+cz+d=0. We know that the plane they span must contain the origin, (0,0,0), which comes from the fact that the span of any two vectors must contain the origin (created by 0u+0v=0). This tells us d=0.
So, I’m searching for a plane of the form ax+by+cz=0. Plugging in u=⎣⎡u1u2u3⎦⎤ tells me that a, b, and c must satisfy
au1+bu2+cu3=0
Similarly, a, b, and c must also satisfy
av1+bv2+cv3=0
Look closely. The left-hand side of both equations looks a lot like the dot product of ⎣⎡abc⎦⎤ with each of u and v. Since those dot products must both be 0 (coming from the right-hand side of each equation), we’re really just looking for a vector ⎣⎡abc⎦⎤ that’s orthogonal to both u and v.
There are infinitely many vectors orthogonal to a particular pair of vectors u and v, meaning there are infinitely many possible values of a, b, and c that satisfy the above equations. (There are more equations than unknowns.)
But, one property that all of these vectors share is that they all point in the same direction – if ⎣⎡abc⎦⎤ is orthogonal to u and v, then so is any non-zero scalar multiple of ⎣⎡abc⎦⎤.
from utils import plot_vectors
import numpy as np
import plotly.graph_objects as go
v1 = (5, 2, 1)
v2 = (-2, 3, 0)
fig = plot_vectors([(v1, "orange", r"u"),
(v2, "#3d81f6", r"v"),
], show_axis_labels=True, vdeltax=1, vdeltay=1)
# Add the plane spanned by v1 and v2
plane_extent = 20
num_points = 3
s_range = np.linspace(-plane_extent, plane_extent, num_points)
t_range = np.linspace(-plane_extent, plane_extent, num_points)
s_grid, t_grid = np.meshgrid(s_range, t_range)
plane_x = s_grid * v1[0] + t_grid * v2[0]
plane_y = s_grid * v1[1] + t_grid * v2[1]
plane_z = s_grid * v1[2] + t_grid * v2[2]
fig.add_trace(go.Surface(
x=plane_x,
y=plane_y,
z=plane_z,
opacity=0.8,
colorscale=[[0, 'rgba(61,129,246,0.3)'], [1, 'rgba(61,129,246,0.3)']],
showscale=False,
))
# Compute the cross product of v1 and v2
v1_arr = np.array(v1)
v2_arr = np.array(v2)
cross = np.cross(v1_arr, v2_arr)
# Draw the line spanned by the cross product through the origin as a dotted line
line_extent = 10
line_points = np.linspace(-line_extent, line_extent, 2)
line_x = line_points * cross[0]
line_y = line_points * cross[1]
line_z = line_points * cross[2]
fig.add_trace(
go.Scatter3d(
x=line_x,
y=line_y,
z=line_z,
mode="lines",
line=dict(color="rgba(0,77,64,0.6)", width=3, dash="dot"),
showlegend=False,
hoverinfo="skip"
),
)
# Add annotation at the midpoint of the line in the same color
mid_idx = len(line_points) // 2
fig.add_trace(
go.Scatter3d(
x=[5],y=[5],z=[5],
mode="text",
text=["All vectors orthogonal to u and v lie on this line"],
textposition="top center",
textfont=dict(color="rgba(0,77,64,0.9)", size=14),
showlegend=False,
hoverinfo="skip"
)
)
fig.update_xaxes(
scaleanchor="y",
scaleratio=1,
tickvals=np.arange(-10, 10, 1)
)
fig.update_yaxes(
scaleanchor="x",
scaleratio=1,
tickvals=np.arange(-10, 10, 1)
)
fig.update_layout(
scene=dict(
xaxis=dict(range=[-10, 10]),
yaxis=dict(range=[-10, 10]),
zaxis=dict(range=[-10, 10]),
aspectmode='cube',
),
width=600,
height=600,
scene_camera=dict(
eye=dict(x=-1.2, y=2, z=1.2)
),
)
fig.show()
Loading...
One particular vector (i.e. set of coefficients a, b, and c) that satisfies the above equations is the cross product of u and v. The cross product of vectors u and v is defined only for vectors in R3, and is given by
There’s a lot of meaning baked into the definition of the cross product, but most of it is more relevant in a traditional engineering or physics context. For example, the cross product is anticommutative, meaning that the order you compute it in matters.
u×v=−(v×u)
That’s the type of statement we won’t bother investigating further here. But, the activities below are important to attempt.
Let’s use the cross product to concretely find the equation of the plane planned by two vectors in R3. Suppose u=⎣⎡521⎦⎤ and v=⎣⎡−230⎦⎤.
The equation of the plane spanned by u and v is then given by
−3x−2y+19z=0
The vector that the cross product returns is sometimes called the normal vector of the plane. Normal is another term for orthogonal or perpendicular. For the plane −3x−2y+19z=0, the normal vector is ⎣⎡−3−219⎦⎤, as that vector is orthogonal to the two vectors u and v that span the plane. When we’re looking at the standard form of the equation of a plane in R3, the normal vector is just the coefficients of x, y, and z in the equation.
The cross product is a construct that only exists in 3-dimensions. Beyond R3, we can’t express planes in standard form, the same way we can’t express lines in R3 in standard form. Instead, we’ll need to resort to their parametric form.
This definition is very similar to the definition of the parametric form of a line in Rn, it’s just that instead of one direction vector, we have two. For instance,
P=⎣⎡3812−7π⎦⎤+s⎣⎡102−100⎦⎤+t⎣⎡52−1310⎦⎤
is a plane in R6, and you should think of it as a 2-dimensional “slice” of 6-dimensional space.
So far, we’ve learned how to think of lines and planes in arbitrarily high dimensions. We can’t visualize a plane in R76, but we have some intuition that it’s a 2-dimensional “slice” of 76-dimensional space.
On the topic of slices:
A line is a 1-dimensional “slice” of 2-dimensional space.
A plane is a 2-dimensional “slice” of 3-dimensional space.
In general, a hyperplane is an (n−1)-dimensional “slice” of n-dimensional space.
Example: 2x1+3x2−5=0 is a hyperplane in R2, defined by the vector a=[23] and b=−5. This is just a line in R2. If it helps, relabel x1 and x2 as x and y.
Example: x1+x2+x3=0 is a hyperplane in R3, defined by the vector a=⎣⎡111⎦⎤ and b=0. This is just a plane in R3. If it helps, relabel x1,x2,x3 as x,y,z.
Hyperplanes are hugely important in machine learning, particularly in the context of classification. You should think of a hyperplane in Rn as a boundary that divides all of Rn into two halves: everything is either above or below.
For example, the hyperplane ⎣⎡−3−219⎦⎤⋅x=0 is shown below. Any point in R3 is either above it, meaning ⎣⎡−3−219⎦⎤⋅x>0, or below it, meaning ⎣⎡−3−219⎦⎤⋅x<0.
I’ll add more to this section on hyperplanes as needed, but I wanted to get this out to you (1) before the midterm and (2) so you have context of what comes after lines and planes.