還記得你高中時第一次接觸代數的情景嗎?
通常,我們會拿到一組方程組,然後被要求解出x和y。
4x - 3y = -2
2x + 7y = 16
兩個方程式、兩個未知數:稍微做點代入,很快就能看出x = 1, y = 2。
再回頭看看前面例子的左邊。它看起來就像一個矩陣[4 -3; 2 7]乘上未知向量[x, y],得到向量[-2, 16]。
以線性代數的記法:A x = b,依循大寫字母代表矩陣、小寫代表向量的慣例。
一個重要的特例是,當所有方程式的右邊都是零。
4x - 3y = 0
2x + 7y = 0
在上面的例子中,唯一的解是x = y = 0,這通常沒什麼意思。
A x = 0的非平凡解,只有在A的行列式為零時才存在。
julia> using LinearAlgebra
julia> A = [4 -3; 2 7]
2×2 Matrix{Int64}:
4 -3
2 7
julia> det(A) # not zero!
34.0
反之,考慮這些方程式:
4x - 3y = 0
8x + 6y = 0
第二個方程式只是第一個的兩倍,並沒有提供新資訊。凡是滿足x = 0.75y的值都會是解。
這些(x, y)值落在二維空間中的一條直線上。解有無限多個,它們構成矩陣的零空間。
在這種情況下,矩陣中的各列並非線性獨立,而矩陣的秩小於列數或行數。
julia> A = [4 -3; 8 -6]
2×2 Matrix{Int64}:
4 -3
8 -6
julia> det(A)
0.0
julia> size(A) # total number of rows and columns
(2, 2)
julia> rank(A) # number of linearly-independent rows (or columns)
1
julia> nullspace(A)
2×1 Matrix{Float64}:
-0.6
-0.8
julia> nullspace(A) |> norm # Julia returns unit vectors when possible
1.0
nullspace()函式會回傳單一個點,並盡可能選擇正規化為單位向量,但這個向量的任何純量倍數同樣也在零空間中。
現在假設你有 1000 個方程式、1000 個未知數?如果這聽起來很蠢,請記得無線數位感測器現在既便宜又多元(量測應變、風速、三軸加速度,什麼都行……)。用 1000 個這樣的感測器來監測像懸索橋這樣的現代結構,再合理不過了。這裡需要有控制系統來解讀資料流,並在情況變得令人擔憂時準備好發出警報。
再次強調,A x = b,其中我們有A(來自工程設計)和b(來自感測器的量測值),但需要求出x。
對不熟悉這件事的人來說,第一個念頭往往是想辦法「把等式兩邊同除以A」,好把A移到右邊。
事實是,A有反矩陣(大多數方陣都有,但並非全部:其行列式必須不為零),而這個計算是可行的(但很慢!)。
julia> A = [4 -3; 2 7]
2×2 Matrix{Int64}:
4 -3
2 7
julia> b = [-2, 16]
2-element Vector{Int64}:
-2
16
# the inverse matrix
julia> inv(A)
2×2 Matrix{Float64}:
0.205882 0.0882353
-0.0588235 0.117647
julia> inv(A) * b
2-element Vector{Float64}:
0.9999999999999999
2.0
可惜的是,計算反矩陣對小矩陣來說很慢,對大矩陣則慢如冰川。如果你的橋在計算過程中崩塌了,那可不理想!
幸運的是,其他演算法快得多(在這個例子中是高斯消去法)。
Julia(沿用 Matlab 的做法)只用一個反斜線來當求解器(技術上稱為「左除」)。
julia> x = A \ b
2-element Vector{Float64}:
1.0
2.0
這是個簡單的例子,但有幾個細節要留意。
rank()指令會給出線性獨立的列數/行數,所以目標是讓它等於未知變數的個數。
julia> rank(A)
2
大概在你還小的時候,有人教過你:要解出N個未知數,就需要由N個方程式組成的方程組。
一如數學裡大多數的事情,實際情況要微妙一些(不只是前一節提到的線性獨立這個細節)。
有N-1個方程式(矩陣A中有N-1列)時,問題是欠定的。我們要絕望地放棄嗎?
這要看情況!
N-1個方程式仍然包含很多資訊。用幾何的角度來說,我們需要N個才能把解確定為 N 維空間中的一個_點_,但pseudoinverse個只會告訴我們解必定落在某條_直線_上。
這麼一來,我們會有無限多個解,但同時也有無限大的參數空間是_沒有_解的。
你解所在的這條直線,是否會穿過一塊有信譽的工程師應該擔心的參數空間區域?或者穿過一塊足以構成理由、讓你把結構對外關閉的區域(懸索橋在強烈側風下可是會晃得很厲害)?也許值得再多做些計算來確定這一點!
julia> A3 = [4 -3 1; 2 2 2; 3 -4 -3]
3×3 Matrix{Int64}:
4 -3 1
2 2 2
3 -4 -3
julia> b3 = [1, 12, -14]
3-element Vector{Int64}:
1
12
-14
# fully-determined solution
julia> A3 \ b3
3-element Vector{Float64}:
1.0
2.0
3.0
# remove row 3 from A3 and B3
julia> A2 = A3[1:2, :]
2×3 Matrix{Int64}:
4 -3 1
2 2 2
julia> b2 = b3[1:2]
2-element Vector{Int64}:
1
12
# an under-determined solution
julia> z1 = A2 \ b2
3-element Vector{Float64}:
1.594594594594594
2.445945945945947
1.9594594594594592
反斜線求解器給了我們一個解!非官方地說,這顯然是範數最小的解(幾何上最接近原點)。
要得到其他解,我們需要A2的nullspace。
把零空間的任何純量倍數加到原本的解上,都會得到另一個有效的解。
# get the nummspace of A2
julia> N = nullspace(A2)
3×1 Matrix{Float64}:
-0.46499055497527725
-0.3487429162314578
0.813733471206735
# add a random multiple of the nullspace to the earlier solution
julia> z = z1 + rand() * N
3×1 Matrix{Float64}:
1.274331860650258
2.205748895487695
2.5199192438620472
# our random z is a valid solution, recovering b2 = [1, 12]
julia> A2 * z
2×1 Matrix{Float64}:
0.9999999999999947
12.0
同樣地,N-2個方程式會把(無限多的)解限制在一個平面上,同樣的原則也適用。
相反的情況是方程式_比_未知數還多,但它們卻仍然線性獨立。
在真實世界的工程裡,這完全是正常的,而且被視為一件好事!
感測器的精度有限,b向量的精度有限,計算中還有雜訊。這時的解就變成對含雜訊資料的最小平方法擬合。
最簡單的技術是用矩陣的偽逆,Julia 把它實作成pinv()函式。
它可以像(非奇異)方陣的反矩陣那樣使用,給出變數的最小平方估計,而不是精確解。
# create 5-row A and b for 2 variables
julia> A5 = [4 -3; 2 7; -1 2; 1 -1; 3 1]
5×2 Matrix{Int64}:
4 -3
2 7
-1 2
1 -1
3 1
julia> rank(A5)
2
# add some random-normal noise to b5
julia> b5n = [-2, 16, 3, -1, 5] + randn(5) * 0.1
5-element Vector{Float64}:
-1.9337349223581917
16.024913008059457
3.0087646177373992
-0.8675748721176717
4.914568047308805
# solve to get x ≈ 1, y ≈ 2, using the pseudoinverse
julia> pinv(A5) * b5n
2-element Vector{Float64}:
1.006117942769543
1.9962973764508272
前一節談的是A x = b的解,等同於我們熟悉的代數。
本節談的是A x = λ x的解,其中 λ 是純量。
這是線性代數特有的概念,但我們可以從幾何上理解它:
對於適當的x和 λ 值,方陣A會把非零向量x的長度縮放 λ 倍,但不改變它的方向(λ 為負時則反向)。
這聽起來很小眾,但結果卻_荒謬地_有用!
術語: λ 的有效值稱為A的_特徵值_,而對應的x值稱為A的_特徵向量_。很遺憾,我們只能接受有些詞會中途換語言這件事。
學生通常會學到如何用手算 2×2 矩陣的特徵值/特徵向量,但用電腦要輕鬆得多(雖然仍舊相當慢,一般情況下對一個n×n矩陣的複雜度是O(n^3))。
julia> A = rand(-9:9, 2, 2)
2×2 Matrix{Int64}:
9 5
3 2
julia> F = eigen(A)
Eigen{Float64, Float64, Matrix{Float64}, Vector{Float64}}
values:
2-element Vector{Float64}:
0.27984674554472466
10.720153254455274
vectors:
2×2 Matrix{Float64}:
-0.497417 0.945605
0.867511 0.325317
# eigenvalues
julia> F.values
2-element Vector{Float64}:
0.27984674554472466
10.720153254455274
# each column is an eigenvector, normalized to a unit vector
julia> F.vectors
2×2 Matrix{Float64}:
-0.497417 0.945605
0.867511 0.325317
一般來說,一個n×n矩陣會有n個特徵值,不過這些值未必_互異_。可以把它們想成一個n次多項式(稱為特徵多項式)的根,這些根可能重複,而且即使矩陣是實數值,它們也常常是複數。
每個特徵向量代表一個方向,而任何純量倍數同樣也是有效的特徵向量。為了方便後續計算,Julia 會回傳範數為 1 的單位向量。
要解釋特徵向量為什麼重要,理想上需要一本 500 頁的教科書,而不是短短幾段文字。這個主題滲透到現代應用數學的_極多_層面。
從最高的層次來看,特徵向量代表資料集中「最重要」的軸(方向)。特徵值則指出每個軸的「相對重要性」(前提是對輸入的正規化做了一些假設)。
這實際上_代表_什麼,取決於應用。
在典型的資料科學問題裡,我們可能會有 100 個以上的「特徵」,以資料行的形式儲存。幾乎無可避免地,會有雜訊、冗餘,以及不想要的相關性。
我們需要做降維,而 PCA 是把混亂整理出秩序的一種方法:
|λ|)由大到小排序特徵向量。k個特徵向量現在就是你的principal components,其中k明顯小於原本的特徵數量。k個軸上,然後開始尋找有趣的模式。以這種形式,PCA 被各種不同的團體使用,從試圖在藥物設計中改善分子性質的醫學科學家,到試圖從民調資料理解選民偏好的政治競選團隊。大概也有想賣你東西的行銷團隊,但任何技術都可以被用在好的或壞的用途上。
PCA 不是最小安裝的 Julia 的一部分,但(在 Exercism 之外)MultivariateStats 套件裡有你需要的東西。
把 PCA 的想法再往前推一步:數位影像不過是像素值的矩陣,我們可以為它們計算主成分。
這在影像壓縮上已經用了幾十年,其中 PCA 是判斷縮減檔案大小時最該保留什麼的指引。
越來越多的情況是,PCA 是影像分類器(例如人臉辨識)的關鍵一環。下次你走過機場時,老大哥不只是在看著你,他還用線性代數來理解他所看到的!
比較不具爭議的是,機械元件的主軸是它的慣性矩張量I的特徵向量(這是矩陣換了個稍微不同的名字)。
你車上的每個輪子大概都有一或多個配重塊,用來把這個張量I的非對角元素歸零。修車廠的技師無疑會避開這些數學(與飛機設計師和火箭工程師相反),但「抖動」只是用來描述張量中交叉項的日常用語;在這個例子裡,這些交叉項會讓你的旅程不那麼舒適,並增加軸承的機械磨損。