早在向量概念中,我們就提過:「陣列可以有任意大小(唯一的限制是你硬體上的記憶體),也可以有任意多個維度。」
在那之後,為了保持簡單,我們大多忽略了一維以上的陣列。 如果你試著完整閱讀複雜無比的 Julia 參考文件,就會更明白我們為什麼這樣決定。
不過,高維陣列在科學運算中_非常、非常_重要,所以我們必須理解它們。
命名慣例: 遵循數百年來的數學慣例,我們把 1 維陣列稱為 Vectors,把 2 維陣列稱為 Matrices。
本文中的例子大多是矩陣。 處理 3 維或更多維度時,語法幾乎完全相同,但在 2 維的螢幕上,輸出結果既難讀又容易讓人困惑。
一個 eltype 為 T 的 N 維陣列,其 type 是 Array{T, N}。
為了方便,也為了與數學命名慣例一致,Julia 定義了一些型別別名:Vector{T} 對應 Array{T, 1},Matrix{T} 則對應 Array{T, 2}。
# 3-D array
julia> m3 = ones(2, 3, 4);
julia> typeof(m3)
Array{Float64, 3}
# Vector
julia> v = [1, 2]
2-element Vector{Int64}:
1
2
julia> typeof(v) == Array{Int64, 1}
true
# Matrix
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> typeof(m)
Matrix{Int64} (alias for Array{Int64, 2})
julia> typeof(m) == Array{Int64, 2}
true
我們已經用很多次向量了:只要把以逗號分隔的清單放進方括號裡就行。分號也可以當作分隔符號。
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> w = [1; 2; 3]
3-element Vector{Int64}:
1
2
3
julia> v == w
true
如果改用空格(或 Tab)當分隔符號,結果就不一樣了。
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
Julia 現在會把它定義成一個 1×3 的矩陣(在其他情境中,我們會稱它為 row vector)。
一般來說,空格會把東西水平接在一起,分號(或換行)則把東西垂直接在一起。
這裡說「東西」是故意含糊的,因為不管你給 Julia 什麼,它都會試著處理。
julia> [v 2v]
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> [v; 2v]
6-element Vector{Int64}:
1
2
3
2
4
6
有 hcat() 和 vcat() 兩個函式可以做同樣的事,而且更明確地表達這是水平與垂直的串接。
更高維度的推廣版本則是 cat() 函式。
jjulia> hcat(v, 2v)
3×2 Matrix{Int64}:
1 2
2 4
3 6
julia> vcat(v, 2v)
6-element Vector{Int64}:
1
2
3
2
4
6
直接輸入矩陣時,用列優先順序很方便,因為這符合人類的直覺(對使用水平書寫的文化來說比較容易閱讀):
julia> m = [1 2 3; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
不過要注意,Julia(和 Fortran、R、Matlab 一樣,但_和_ C/C++ 或 NumPy 不同)是以行優先順序儲存 N 維陣列的,如果你用迴圈走訪元素,這會造成很大的效能差異。 幫你的 CPU 快取一把,它也會幫你一把!
# put these integers in 2 rows and 3 columns
julia> reshape(collect(1:6), 2, 3)
2×3 Matrix{Int64}:
1 3 5
2 4 6
上面的例子把整數 1 到 6 沿著行方向填進一個 2×3 的矩陣。
有各種工具函式可以用來建立常見的陣列(均勻或隨機)。
julia> zeros(2, 3) # see also ones()
2×3 Matrix{Float64}:
0.0 0.0 0.0
0.0 0.0 0.0
julia> falses(2, 2) # booleans, see also trues()
2×2 BitMatrix:
0 0
0 0
julia> rand(Float32, 2, 3) # random numbers in the interval [0, 1)
2×3 Matrix{Float32}:
0.768823 0.169633 0.632565
0.388451 0.109176 0.850381
熟悉 NumPy 的人都知道,np.linspace 是一個廣為使用的函式,可以產生指定長度、指定端點、數值等間距的陣列。
它通常用來當作圖表的 x 軸,或是線性模型中的自變數。
Julia 沒有完全對應的函式,但非常有彈性的 range() 只要指定上下界,再加上 length 這個關鍵字引數,就能模擬它的效果。
任何在本機工作、能取用 Plots 套件的人,都可以執行這段程式碼:
julia> using Plots
julia> x = range(0, 2π; length=100)
0.0:0.06346651825433926:6.283185307179586
julia> typeof(x)
StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}
julia> vals = [x sin.(x) cos.(x)]
100×3 Matrix{Float64}:
0.0 0.0 1.0
0.0634665 0.0634239 0.997987
0.126933 0.126592 0.991955
0.1904 0.189251 0.981929
(...truncated)
julia> plot(vals[:, 1], vals[:, 2:3])
StepRangeLen 可以像向量一樣使用,也可以轉換成矩陣的其中一行。
另請參閱功能對應的 logrange() 函式,它可以在對數軸上產生等間距的值。
對 2 維陣列來說,我們通常依照 [row, col] 的順序使用兩個索引。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[1, 2] # row 1, col 2
2
# Stay within bounds! There is no row 3.
julia> m[3, 1]
ERROR: BoundsError: attempt to access 2×3 Matrix{Int64} at index [3, 1]
julia> m[3]
2
最後一個例子或許令人意外:只給_一個_索引並不算錯誤,而且會回傳單一元素。
這要回到前面關於行優先順序的說明:Julia 會先沿著第 1 行往下走,接著第 2 行,直到在_記憶體中_找到第 3 個元素。
小心:在撰寫通用函式庫時這偶爾有用,但更可能讓人一頭霧水!
查詢陣列大小時也會遇到類似的問題。
length() 會給出元素的總數,size() 則會回傳一個包含 ndims() 個元素的元組,每個元素是對應維度的長度。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> length(m)
6
julia> size(m) # 2 rows, 3 cols
(2, 3)
julia> ndims(m) # how many dimensions? Like `m |> size |> length`
2
我們可以輕鬆複製一個子矩陣。
在下面的例子中,reshape() 函式會沿著行方向填入,把陣列調整成指定的維度,接著我們再對它切片。
julia> m12 = reshape(collect(1:12), 4, 3)
4×3 Matrix{Int64}:
1 5 9
2 6 10
3 7 11
4 8 12
julia> m12[2:4, 1:2]
3×2 Matrix{Int64}:
2 6
3 7
4 8
# some rows, all columns
julia> m12[2:4, :]
3×3 Matrix{Int64}:
2 6 10
3 7 11
4 8 12
單獨一個 : 代表「複製這個維度中的所有內容」。
如果要取不連續的列或行,就用向量:
julia> m12[[1, 3], :] # rows 1 and 3
2×3 Matrix{Int64}:
1 5 9
3 7 11
單獨一個 : 會沿著行方向,把任何陣列攤平成向量。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m[:]
6-element Vector{Int64}:
1
4
2
5
3
6
我們之前看過把 sum()、maximum() 這類聚合函式套用在 1 維集合上,它們會對所有元素運算,並回傳一個純量結果。
這在高維度也同樣適用。
不過,我們有時只想把函式套用在某一個維度上,例如沿著某個方向加總,回傳一個帶有大小為 1 的 singleton dimension 的陣列。
為此,有一個選用的 dims 關鍵字引數。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> sum(m) # sum everything
21
julia> sum(m; dims=1) # sum down
1×3 Matrix{Int64}:
5 7 9
julia> sum(m; dims=2) # sum across
2×1 Matrix{Int64}:
6
15
# other aggregation functions are similar
julia> maximum(m; dims=2)
2×1 Matrix{Int64}:
3
6
dims 的值就是_變成單一維度的那個維度_,所以在上面的例子中 dims=1 => 1×3、dims=2 => 2×1。
延伸來看,如果把 dims 設成一個陣列或範圍,高維陣列也能一次縮減多個維度_(同樣地,要理解輸出結果可能得動點腦筋!)_。
dims 關鍵字引數在 sum() 這類內建函式中很常見,但我們要怎麼在自己的程式碼裡寫出對等的功能呢?
一個好方法是使用 reduce() 這類高階函式,這會在之後的某個概念中詳細介紹。
另一個做法是,Julia 提供了幾個函式,讓我們可以把 N 維陣列當成巢狀的向量來處理。
對矩陣來說,eachrow() 和 eachcol() 很方便,但更通用的函式是 eachslice(),它可以處理任意維度。
# m is as in the previous examples
julia> eachrow(m)
2-element RowSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Int64, Base.Slice{Base.OneTo{Int64}}}, true}}:
[1, 2, 3]
[4, 5, 6]
julia> eachcol(m)
3-element ColumnSlices{Matrix{Int64}, Tuple{Base.OneTo{Int64}}, SubArray{Int64, 1, Matrix{Int64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Int64}, true}}:
[1, 4]
[2, 5]
[3, 6]
這個型別看起來有點嚇人,但這只是因為它是原始陣列的一個_檢視_,讓我們不用複製就能操作它。 Julia 的陣列大小可能高達數 TB,所以複製可能會是場效能噩夢!
這樣的檢視可以用來跑迴圈、廣播,或是我們到目前為止在教學大綱中看過的所有其他操作。
此外,comprehension 搭配陣列輸入時可以既強大又靈活。 簡單的情況在迴圈概念中提過,之後的概念會有更完整的討論。
我們在向量運算概念中討論過 1 維的情況。
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v .- 0.5
3-element Vector{Float64}:
0.7
1.0
1.2
帶點的運算子 .- 會藉由broadcasting,把單一的 0.5 視為等同於 [0.5, 0.5, 0.5] 以符合維度,接著再逐元素做減法。
要把這個概念延伸到更高維度,其實只是同樣的事情再更多一些。
舉例來說,把 2x1 的列向量 [1.0 1.5 2.0] 與 2x3 的矩陣 [1 2 3; 4 5 6] 相乘並廣播時,等同於先把 [1.0 1.5 2.0] 廣播成 [1.0 1.5 2.0; 1.0 1.5 2.0],再逐元素做乘法。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> m .* [1.0 1.5 2.0] # broadcast a 1x3 row vector down
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m .* [1.0 1.5 2.0; 1.0 1.5 2.0] # 2x3 elementwise multiplication
2×3 Matrix{Float64}:
1.0 3.0 6.0
4.0 7.5 12.0
julia> m ./ [1, 2] # broadcast a 2x1 column vector across
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
julia> m ./ [1 1 1; 2 2 2] # 2x3 elementwise division
2×3 Matrix{Float64}:
1.0 2.0 3.0
2.0 2.5 3.0
注意: 廣播時,必須大小相符的是那些非單一維度(例如 2x3 Matrix .* 2x1 Matrix)。
此外,函式也可以被廣播到 Matrix 的每個元素,做法和對 Vector 完全一樣。
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> (x -> x^2).(m) # broadcast a function to all elements
2×3 Matrix{Int64}:
1 4 9
16 25 36
一開始你可能會覺得,程式設計師被搞混的空間會隨著維度數量呈指數成長,但多練習很有幫助。 另外,即使語法不同,熟悉 NumPy 陣列的人也能很快上手 Julia。
許多對向量與矩陣的常見操作,都歸在線性代數這個大範疇之下。
Exercism 網站上(目前)還有兩個額外的概念,在我們設計出能搭配的練習之前,它們都只有文件。
未來可能還會有第三個關於矩陣分解的概念。
不過請注意,線性代數基礎相當數學,未必適合所有學生。
後面幾個概念(方程式求解與矩陣分解)更進階,真正鎖定的對象是對大學程度數學感到自在的人。
很抱歉,但這就是這個科目的本質,而它也正是 Julia 一個重要的應用場景。