學習軌道
/
Julia
Julia
/
練習
/
Exercism 矩陣
Exercism 矩陣

Exercism 矩陣

學習練習

簡介

早在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表示,「點」則是非零值。

1. 定義 Exercism 標誌的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

2. 定義讓標誌皺眉的函式

定義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

3. 拼出一面貼紙牆

定義函式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

4. 把點改成各行的像素數量

我們不一定要用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;
]

5. 渲染點矩陣

與其只是空談怎麼渲染我們的作品,我們希望能實際做出來,方便觀看。定義函式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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Julia Exercism

準備好開始 Exercism 矩陣 了嗎?

註冊 Exercism,透過 35 個概念128 個練習 和真人引導來學習並精通 Julia,全部免費。