什麼是「線性代數」?
技術性的定義有很多,散見於[維基百科][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-範數。
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」,挑一部符合你偏好的風格、詳細程度和語言的來看。
兩個向量的點積取決於它們長度相等。
延伸來看,矩陣乘法中左邊矩陣的_欄數_必須與右邊矩陣的_列數_相符。
把大小表示成 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 個單位。 為了初始化它的位置,我們需要取出這三個方向向量,將它們正規化,然後放進一個矩陣裡。
實作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