學習軌道
/
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-範數。 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」,挑一部符合你偏好的風格、詳細程度和語言的來看。

維度

兩個向量的點積取決於它們長度相等。

延伸來看,矩陣乘法中左邊矩陣的_欄數_必須與右邊矩陣的_列數_相符。

把大小表示成 size(A) 所輸出的 (nrows, ncols) 元組,我們就有 (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 有時被稱為_內積_。

旋轉

一種特別常見的矩陣乘法涉及旋轉矩陣。

在 2D 中,有一個相對簡單的矩陣可以將向量逆時針旋轉 θ 弧度。

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,全部免費。