早在Vectors概念的內容中,我們就提過「陣列可以有任意大小(只取決於硬體記憶體的限制),而且可以有任意多個維度。」
從那之後,為了保持簡單,我們大多略過了超過一個維度的陣列。 如果你試著去讀 Julia 參考文件的全貌,就會更明白這個決定。
不過,高維陣列在科學運算中_非常、非常_重要,所以我們必須了解它們。
術語說明: 遵循數百年來的數學慣例,我們把一維陣列稱為Vectors、二維陣列稱為Matrices。
本文中的例子大多是矩陣。 處理三個以上的維度時,語法幾乎完全相同,但輸出(在二維螢幕上)卻難以閱讀又容易混淆。
我們已經建立過很多向量,作法是把以逗號分隔的清單放進方括號裡。 分號也可以當作分隔符號。
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 的 Matrix(在其他情境中,我們會稱它為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 的 Matrix。
有各種工具函式可以建立常見的陣列(均勻或隨機)。
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
對二維陣列來說,我們通常使用兩個索引,順序是 [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
我們先前看過把sum()和maximum()這類聚合函式套用在一維集合上,它們會處理所有元素並回傳一個純量結果。
這在高維度也適用。
不過,我們可能只想把函式套用到某一個維度,例如沿著垂直或水平方向加總,回傳一個大小為 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的值就是_要縮成大小 1 的那個維度_,所以在上面的例子中 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,所以複製很可能是一場效能災難!
像這樣的檢視可以用來做迴圈、廣播,或是我們目前在課程大綱裡看過的所有其他操作。
另外,推導式搭配陣列輸入可以既強大又靈活。
簡單的情況在Loops概念中提過,之後的概念會有更完整的討論。
我們在Vector Operations概念中討論過一維的情況。
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
帶點的運算子 .- 會把大小為 1 的 0.5 經由broadcasting調整成符合維度的大小,將它視為等同於 [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
注意: 要進行廣播,非單一(大小不為 1)的維度必須大小相符(例如 2x3 Matrix .* 2x1 Matrix)。
此外,函式也可以像套用到Vector一樣,廣播到Matrix的每個元素。
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 上。
點矩陣是由點和空白組成的二維影像。點在空白的背景上構成想要的影像。
點矩陣影像可以在記憶體中以二維的Matrix儲存與操作。接下來,矩陣中的「空白」會以0表示,「點」則是非零值。
Matrix
Matrix的樣子如下,其中0代表空白,1代表點:
[
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 1 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0;
0 1 0 0 1 0 1 0 0 0 0 1 0 1 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0;
0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 1 0;
0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0;
0 0 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0;
]
如果你把它「渲染」出來,看起來會是這樣:
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
定義frown!()和frown()兩個函式,它們接收 Exercism 標誌的Matrix。回傳一個把微笑的嘴巴換成皺眉嘴巴的Matrix。
產生的Matrix渲染起來會像這樣:
XX XX
X X
X X X X
X X X X X X
X X
X X
X XX X
X X X X
X X X X
X X
XX XX
定義函式stickerwall(),它接收 Exercism 矩陣作為輸入。回傳一個點矩陣的Matrix,渲染起來如下:
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X X X X X XX X
X X X X X X X X
X XX X X X X X
X X X X
XX XX XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XX XX XX XX
X X X X
X X X X X X X X
X X X X X X X X X X X X
X X X X
X XX X
X XX X X X X X
X X X X X X X X
X X X X X XX X
X X X X
XX XX XX XX
我們不一定要用1當作點,所以只要願意,也可以把其他有用的資訊編碼進去。定義函式colpixelcount(),它接收任何以1為點的點矩陣作為輸入。回傳一個大小相同的點矩陣,其中每一行的點就是該行點的數量。
以 Exercism 標誌的Matrix作為輸入時,輸出會是如下的Matrix:
[
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 8 0 0 0 1 0 0 0 0 0 0 1 0 0 0 8 0;
0 8 0 0 1 0 2 0 0 0 0 2 0 1 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1;
0 8 0 0 0 0 2 0 0 0 0 2 0 0 0 0 8 0;
0 8 0 0 0 0 0 1 0 0 1 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 1 1 0 0 0 0 0 0 8 0;
0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 0;
0 0 2 2 0 0 0 0 0 0 0 0 0 0 2 2 0 0;
]
與其只是空談怎麼渲染我們的作品,我們希望能實際做出來,方便觀看。定義函式render(),它接收一個點矩陣作為輸入。回傳一個字串,其中點會渲染成'X',0會渲染成' ',並以換行符號連接每一列。
julia> render(E)
" XX XX \n X X \n X X X X \n X X X X X X \n X X \nX X\n X X X X \n X X X X \n X XX X \n X X \n XX XX "
列印出來時,應該就會如預期地渲染出 Exercism 矩陣。
julia> print(render(E))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX
這也應該適用於不同於1的「點」:
julia> print(render(colpixelcount(E)))
XX XX
X X
X X X X
X X X X X X
X X
X X
X X X X
X X X X
X XX X
X X
XX XX