轨道
/
Julia
Julia
/
练习
/
机器人革命
机器人革命

机器人革命

学习练习

简介

什么是“线性代数”?

关于它的技术性定义有很多,在[维基百科][linalg-wiki]上、在教科书里,也在像 [3Blue1Brown][3blue1brown] 这样优秀的网站上都能找到。不过就我们的目的而言,可以讲得更随意一些。

Note

线性代数讲的是你能用向量和矩阵做的许多有趣(而且非常有用)的事情。

Julia(和 Python)的维护者很喜欢这类东西。 而 Exercism 的管理层就_没那么热衷_了。

这个概念只会涉及这门庞大主题中较简单的部分,但先提醒一句:它不可避免地是一个相当偏数学的概念。

什么是向量?

这取决于你问的是谁,也取决于你想怎样把相当抽象的东西可视化。

  • 在 Julia 里,Vector{T}只是 Array{T, 1}的别名,eltype T可以是任何类型。
  • 在(大部分)物理学中,向量是 N 维空间里一支带有 length 和 direction 的箭头,但没有固定的位置。
  • 在线性代数中,物理学家所说的那支箭头被固定在空间中,尾部位于原点。这样一来箭杆就成了多余的,于是我们可以把向量表示成 N 维空间中的一个 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 年里都花了大把时间,把各种计算变成一连串矩阵乘法。

这是计算机非常擅长的运算:

  • 它具有高度的重复性。
  • 它可以被高效地并行化。
  • 人们开发了大量专用硬件来让它更快,包括很可能已经集成在你笔记本电脑里的 GPU。

其中的细节相当简单,虽然乍看之下不太直观。

设想用矩阵 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. 确定机器人的朝向

为了记录机器人的朝向和伸展状态,它有三个标记点,它们与机器人中心的距离都是 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

2. 旋转机器人

接下来,我们需要一些功能来改变运动方向。为此,我们需要旋转机器人,让它朝向要前往的地方。

实现 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

3. 检查朝向是否正确

要把机器人从一个位置移动到另一个位置,我们得先检查它的朝向是否正确,再移动它。朝向矩阵的第二列表示机器人面朝的前方方向。

实现 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

4. 机器人的身体坐标

由于朝向矩阵也会记录机器人的形状,我们需要知道在移动机器人之后,这些点相对于原点的位置。这有助于机器人在移动时避开与其他物体的碰撞。

实现 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
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Julia Exercism

准备好开始 机器人革命 了吗?

注册 Exercism,借助 35 个概念128 个练习 和真人导师指导,学习并掌握 Julia,全部免费。