什么是“线性代数”?
关于它的技术性定义有很多,在[维基百科][linalg-wiki]上、在教科书里,也在像 [3Blue1Brown][3blue1brown] 这样优秀的网站上都能找到。不过就我们的目的而言,可以讲得更随意一些。
线性代数讲的是你能用向量和矩阵做的许多有趣(而且非常有用)的事情。
Julia(和 Python)的维护者很喜欢这类东西。 而 Exercism 的管理层就_没那么热衷_了。
这个概念只会涉及这门庞大主题中较简单的部分,但先提醒一句:它不可避免地是一个相当偏数学的概念。
这取决于你问的是谁,也取决于你想怎样把相当抽象的东西可视化。
Vector{T}只是 Array{T, 1}的别名,eltype T可以是任何类型。length 和 direction 的箭头,但没有固定的位置。point,其中 N个元素表示沿 N个轴各自到原点的距离。就目前而言,先忽略由字符串或字符构成的向量。
在这个概念里,我们会用到数值类型:Int、Float 或 Complex。
同样,你会看到各种各样的答案。
linear combination。transformation,就像 function 作用到其他输入类型上一样。有些方阵足够常见,以至于有了专门的名称。
对角矩阵: 所有非零元素都在 main diagonal 上(从左上到右下)。
julia> [1 0 0; 0 2 0; 0 0 3]
3×3 Matrix{Int64}:
1 0 0
0 2 0
0 0 3
# a convenient shortcut
julia> diagm(1:3)
3×3 Matrix{Int64}:
1 0 0
0 2 0
0 0 3
单位矩阵: 对角线上全是 1 的对角矩阵(原因以后会更清楚)。
通常缩写为 I。
julia> Matrix{Float64}(I, 3, 3)
3×3 Matrix{Float64}:
1.0 0.0 0.0
0.0 1.0 0.0
0.0 0.0 1.0
上三角: 对角线及其上方为非零值,下方为零。 下三角 想必你能猜到。
如果你确实想把行和列互换,permutedims() 函数就能做到,并给你一个新矩阵。
这需要复制,处理大矩阵时既慢又费内存。
就线性代数的用途而言,transpose() 函数更有用,因为它会很快地在原矩阵上创建一个惰性包装。
更有用的是 adjoint() 函数,它还会把复数的虚部符号取反。
这个操作足够常用,所以我们只要在变量名后面加一个撇号 ' 就能得到共轭转置。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
# new, full copy
julia> permutedims(m)
3×2 Matrix{Int64}:
1 4
2 5
3 6
# lazy version
julia> transpose(m)
3×2 transpose(::Matrix{Int64}) with eltype Int64:
1 4
2 5
3 6
julia> mc = [1+2im 2+3im; 3+2im 1+2im]
2×2 Matrix{Complex{Int64}}:
1+2im 2+3im
3+2im 1+2im
# lazy conjugate transpose
julia> adjoint(mc)
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
1-2im 3-2im
2-3im 1-2im
# syntactic sugar for adjoint
julia> mc'
2×2 adjoint(::Matrix{Complex{Int64}}) with eltype Complex{Int64}:
1-2im 3-2im
2-3im 1-2im
本文档余下的内容会用到 LinearAlgebra 模块的功能。
一行 using LinearAlgebra 就能把它引入命名空间,但我们在示例里不会反复写出这一行(视觉上太杂乱了)。
这在[向量运算][vector-ops]这个概念中已经讨论过。
运算符是 .*,它逐对作用于输入向量,输出的尺寸和类型与输入相同。
julia> [1, 2] .* [3, 4]
2-element Vector{Int64}:
3
8
这个极其常见的运算在教科书里写作 u ⋅ v,也就是所谓的“点积”。
点积等价于逐元素乘积的_和_。
两个向量可以用通常的 * 运算符相乘,但前提是左边的向量是 adjoint:方便地写作 u' * v。
等看到后面关于矩阵乘法的章节,这一点应该会更清楚。
上移的(居中)点号在 Julia 中就能用(输入 \cdot 再按 Tab 键),它是 dot() 函数的语法糖。
这样就不需要指定共轭转置,这个细节会自动处理好。
julia> using LinearAlgebra
# with element-wise syntax
julia> sum([1, 2] .* [3, 4])
11
# with u' * v syntax
julia> [1, 2]' * [3, 4]
11
# with dot()
julia> dot([1, 2], [3, 4])
11
# with \cdot syntax
julia> [1, 2] ⋅ [3, 4]
11
对于复值向量,左边的向量需要取共轭(虚部符号取反)。
dot函数会自动完成,u'语法则显式完成,但如果 u和 v是复数,sum(u .* v)就会出错。
一个向量有多“大”?
norm就是试图抓住这一点,把向量归约成一个合适的标量。
范数定义了一整个家族,但最常见得多的是 2-范数,即 √(v ⋅ v)。
这种均方根运算就是从原点出发的_毕达哥拉斯距离_(在 N 维空间中)。 如果我们把向量想象成一支尾部在原点的箭头,那么 2-范数就是这支箭的_长度_。
任何 p-范数都可以通过把 p作为第 2 个实参传入算出来。
1-范数有时很有用:它就是各元素绝对值之和(因此算起来非常快、非常简单)。
# defaults to the 2-norm
julia> norm([1, 2, 3])
3.7416573867739413
# the 1-norm
julia> norm([1, -2, 3], 1)
6.0
有时候看起来,全世界每一位应用数学家在过去 80 年里都花了大把时间,把各种计算变成一连串矩阵乘法。
这是计算机非常擅长的运算:
其中的细节相当简单,虽然乍看之下不太直观。
设想用矩阵 A去乘向量 v,得到输出 w(按照线性代数的惯例,我们用大写字母表示矩阵,用小写字母表示向量)。
A的第一行与 v做点积,得到 w的第一个元素;第二行得到第二个元素,依此类推。
julia> A = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4
julia> v = [5, 6]
2-element Vector{Int64}:
5
6
julia> A * v
2-element Vector{Int64}:
17 # equals [1, 2] ⋅ [5, 6]
39 # equals [3, 4] ⋅ [5, 6]
矩阵 * 矩阵的乘法把这一点扩展到右边矩阵的每一列。
对于 C = A * B,可以认为 A分别去乘 B的每一列,得到 C对应的列:也就是一连串矩阵与向量的乘法。
等价地说,也可以认为 A的第一行分别与 B的每一列做点积,得到 C的第一行;第二行得到第二行,依此类推。
julia> A
2×2 Matrix{Int64}:
1 2
3 4
julia> B = [5 7; 6 8]
2×2 Matrix{Int64}:
5 7
6 8
# left column is the same as A*v previously
julia> A * B
2×2 Matrix{Int64}:
17 23
39 53
julia> B * A
2×2 Matrix{Int64}:
26 38
30 44
如上面的例子所示,矩阵乘法_不满足交换律_:A*B和 B*A之间没有简单的关系。
对刚接触矩阵乘法的人来说,光靠读这些文字大概很难想象它的样子。 YouTube 上有很多用图形演示它的视频,所以搜索 “matrix multiplication”,再挑一个你喜欢的风格、详细程度和语言就好。
两个向量的点积要求它们长度相等。
由此推广,矩阵乘法中左边矩阵的_列数_必须与右边矩阵的_行数_相匹配。
把尺寸写成 (nrows, ncols) 这样的元组(也就是 size(A)的输出),于是我们有 (a, b) * (b, c) -> (a, c)。
“内侧”的维度,这里是 b和 b,彼此兼容,可以做点积。
“外侧”的维度,这里是 a和 c,决定输出的维度。
一个用矩形矩阵的例子:
julia> D = reshape(1:6, 2, 3)
2×3 reshape(::UnitRange{Int64}, 2, 3) with eltype Int64:
1 3 5
2 4 6
julia> E = reshape(1:12, 3, 4)
3×4 reshape(::UnitRange{Int64}, 3, 4) with eltype Int64:
1 4 7 10
2 5 8 11
3 6 9 12
julia> D * E
2×4 Matrix{Int64}:
22 49 76 103
28 64 100 136
# (3, 4) * (2, 3) not possible
julia> E * D
ERROR: DimensionMismatch: matrix A has axes (Base.OneTo(3),Base.OneTo(4)), matrix B has axes (Base.OneTo(2),Base.OneTo(3))
以矩阵的方式相乘一对向量,有两种可能。
按惯例,我们会用 u' * v来等价于点积。
它的维度是 (1, 3) * (3, 1),而 Julia 会把 (1, 1)的输出简化成一个标量(例如,R 就不会)。
另一种做法是用 u * v',其维度为 (3, 1) * (1, 3) -> (3, 3),这样会乘遍所有可能配对的元素,把向量扩展成一个矩阵。
julia> u = [1, 2]
2-element Vector{Int64}:
1
2
julia> v = [3, 4]
2-element Vector{Int64}:
3
4
julia> u' * v
11
julia> u * v'
2×2 Matrix{Int64}:
3 4
6 8
u' * v有时被称为_内积_。
有一种特别常见的矩阵乘法涉及旋转矩阵。
在二维中,有一个相对简单的矩阵,可以把向量逆时针旋转 θ 弧度。
julia> rot2d(θ, vec) = [cos(θ) -sin(θ); sin(θ) cos(θ)] * vec
rot2d (generic function with 1 method)
# unit vector in the x direction
julia> i_hat = [1, 0]
2-element Vector{Int64}:
1
0
# rotate 45 degrees
julia> rot2d(π/4, i_hat)
2-element Vector{Float64}:
0.7071067811865476
0.7071067811865475
# rotate 90 degrees -> unit vector in the y direction, j_hat
julia> rot2d(π/2, i_hat)
2-element Vector{Float64}:
6.123233995736766e-17 # zero, within numerical error
1.0
你正在为一家机器人初创公司工作,该公司正在开发一个简易机器人作为概念验证。你的任务是提供一些功能,用来控制机器人的运动。
为了记录机器人的朝向和伸展状态,它有三个标记点,它们与机器人中心的距离都是 1 个单位。要初始化它的位置,我们需要取这三个方向向量,将它们归一化,然后放进一个矩阵里。
实现 orientrobot(vectors) 函数,它接收一个由三个向量组成的向量。返回一个 2x3 矩阵,其中的列是归一化后的向量。
julia> orientrobot([[-1,1],[1,0],[-1,-1]])
2×3 Matrix{Float64}:
-0.707107 1.0 -0.707107
0.707107 0.0 -0.707107
接下来,我们需要一些功能来改变运动方向。为此,我们需要旋转机器人,让它朝向要前往的地方。
实现 rotaterobot(orientation, θ) 函数,它接收机器人的朝向矩阵和一个角度 θ,用于绕逆时针方向旋转。返回新的朝向矩阵。
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> rotaterobot(orientmatrix, π/2)
2×3 Matrix{Float64}:
-0.707107 6.12323e-17 0.707107
-0.707107 1.0 -0.707107
要把机器人从一个位置移动到另一个位置,我们得先检查它的朝向是否正确,再移动它。朝向矩阵的第二列表示机器人面朝的前方方向。
实现 robotoriented(orientation, direction) 函数,它接收一个朝向矩阵和一个相对位置向量。如果机器人的朝向与相对位置向量的方向相同(在舍入误差范围内),返回 true。
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> robotoriented(orientmatrix, [5, 0])
true
julia> robotoriented(orientmatrix, [0, 5])
false
julia> robotoriented(orientmatrix, [-5, 0])
false
由于朝向矩阵也会记录机器人的形状,我们需要知道在移动机器人之后,这些点相对于原点的位置。这有助于机器人在移动时避开与其他物体的碰撞。
实现 bodylocation(orientation, position) 函数,它接收一个朝向矩阵和机器人中心的当前位置。返回平移后的朝向矩阵。
julia> orientmatrix = initialize([[-1,1],[1,0],[-1,-1]]);
julia> bodylocation(orientmatrix, [5, 3])
2×3 Matrix{Float64}:
4.29289 6.0 4.29289
3.70711 3.0 2.29289