軌道
/
Julia
Julia
/
課程大綱
/
向量運算
向量

向量運算 在 Julia

2 個練習

關於 向量運算

在Vectors概念中,我們說過「陣列是 Julia 語言的核心」,而向量是一維陣列。

既然如此,我們可以合理期待這個語言提供許多靈活且強大的方式來_操作_向量,無論這代表什麼。

關於術語的說明:雖然本文大量談論「向量」,但其中許多內容也適用於任何可疊代型別:範圍、元組、集合,以及各種其他型別。

接受向量輸入的函式

有些非常簡單的函式會接受向量輸入,並且(對於一維輸入)回傳純量輸出。

v = [2, 3, 4]
length(v)  # => 3
sum(v)  # => 9

當我們讀到多維陣列概念時,就會更清楚知道這是_降維_,而不一定是回傳純量。 如果這對你來說毫無意義,現在先不用擔心。

這類函式還有很多。 請參閱Statistics概念來看看一些例子。

也有作用於多個向量的函式,例如(非常實用的)zip。

julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])

# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
 (1, 'a', "I")
 (2, 'b', "make")
 (3, 'c', "tuples")

zip()接受任意數量的類向量輸入,並回傳由元組組成的疊代器。

這些輸入通常長度都相同。 如果其中一個較短,其他輸入會被截斷到最短的長度:_也許_這是你想要的結果,但_更常見_的情況是你程式碼裡的 bug。

算術

假設你有一個數值向量,想從每個值減去 0.5。

julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
 1.2
 1.5
 1.7

julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)

這樣會失敗,那如果是減去另一個向量呢?

julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

成功了,但當向量變長時,這種做法相當繁瑣且耗記憶體。

根據你在課程大綱中學到哪裡,你大概可以想到其他做法:

  • 寫一個迴圈,不過這樣會很冗長又笨重。
  • 使用推導式:[x - 0.5 for x in v]可以得到想要的結果(Python 風格)。
  • 使用高階函式:map(x -> x - 0.5, v)也有效(Haskell 風格,不過在許多語言中都很常見)。

幸運的是,Julia 有一個「神奇」的點,可以非常簡單地解決這個問題:v .- 0.5就是你需要的一切。

下一節會解釋為什麼。

廣播

所以,v - 0.5會失敗,但v .- 0.5會成功,而我們需要了解這個點在做什麼。

它做了兩件事,兩者結合起來就能得到想要的結果。

1)逐元素套用

首先,在任何中綴運算子_前面_加上一個點,代表「分別對每個元素套用這個運算」。

同樣地,在函式名稱_後面_加上一個點會將其「向量化」,即使該函式原本是為純量輸入而寫的。

julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
 1.0
 2.0
 3.0

順帶一提,中綴運算子其實只是底層函式的語法糖。

這代表,舉例來說,[1, 5, 10] .% 3會被翻譯員轉譯成 mod.([1, 5, 10], 3),然後mod.語法會執行(兩個版本都回傳[1, 2, 1])。

2)單一元素擴展

我們在前面的例子看到,可以減去長度相同的向量,但請理解.-是比-更_安全_的運算子,因為它清楚表達了逐元素的意圖。

julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

那長度不同的向量呢?

julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size

julia> v .- [0.5,]
3-element Vector{Float64}:
 0.7
 1.0
 1.2

一般來說,長度不同會產生錯誤,_除非_其中一個長度為 1(技術上稱為「單一元素」維度)。

像[0.5,]或單純0.5這樣的單一元素會透過重複自動擴展到所需的長度。 這就是broadcasting的核心。

熟悉其他語言中廣播的程式設計師應該注意,Julia 的做法(大致上)類似 NumPy,但對大小不符的容忍度比 R 低得多。

原地廣播

如果記憶體用量是個問題,那麼原地操作是減少配置的常見做法。 然而,上述範例中的廣播操作會建立新的Vector,而不是修改原始向量。

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 1
 2
 3

若要原地修改v,修改動作必須廣播。

julia> v .= v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v .+= 1  # equivalent operation to above
3-element Vector{Int64}:
 3
 4
 5

julia> v
3-element Vector{Int64}:
 3
 4
 5

但請小心!賦值運算子.=前面的點很重要。

julia> v = [1, 2, 3];

julia> v = v .+ 1
3-element Vector{Int64}:
 2
 3
 4

julia> v
3-element Vector{Int64}:
 2
 3
 4

這看起來運作方式相同,但這裡v .+ 1建立了一個新向量[2, 3, 4],然後將它指定給變數v,而最初的向量[1, 2, 3]則留在記憶體中等待垃圾回收。 這最終會使用前一個範例兩倍的記憶體,而前一個範例重複使用了最初向量所配置的記憶體。

同樣地,當廣播的向量大小相同時,也有一些相關的細微差別。 例如,給定兩個大小相同的向量v和w:

  • v .= w會在v的記憶體位置產生w的copy。v的後續變更不會影響w,反之亦然。
  • v = w會產生另一個指向w記憶體位置、名為v的指標。v的後續變更會反映在w上,反之亦然。

未加點的運算子:一則警世故事

本小節數學成分較重,因此大多數學生不需要真正理解它。 不過,這是個有用的警告,當你看到意外的錯誤訊息時,可能有助於除錯。

julia> v = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})

# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
 1  2  3

julia> u * v
1-element Vector{Int64}:
 14

julia> v * u
3×3 Matrix{Int64}:
 1  2  3
 2  4  6
 3  6  9

如果你剛好有線性代數的背景,那麼(1)你不是典型的 Exercism 使用者_(但非常歡迎你!)_,而且(2)你可能會認出v是列向量,u是行向量,u * v是內積,v * u是外積。 Julia 在這個地方和在其他所有地方一樣,都遵循數學規則。

**給其他人:**請了解為什麼我們建議你應該一律使用加點的運算子來進行逐元素計算:v .* v的運作方式完全符合你的預期,會得到[1, 4, 9]。

索引

在之前的概念中已經討論過用索引編號選取向量元素。

a = collect('A':'Z')  # => 26-element Vector{Char}

# index with an integer
a[2]  # => 'B'

# index with a range
 a[12:2:18]  # => ['L', 'N', 'P, 'R']
 
 # index with another vector
 a[ [1, 3, 5] ]  # => ['A', 'C', 'E']

邏輯索引

也可以選取滿足某些邏輯運算式(技術上稱為「述詞」)的元素。 這通常需要廣播。

julia> a[a .< 'D']
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)

對於更複雜的運算式,點通常會不斷增生(但它們很小,而且很容易輸入)。

julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
 'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
 'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
 'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)

提醒一下,「向量」實際上可以是任何適當的有序可疊代物件,例如範圍:

julia> n = 3:10
3:10

julia> n[isodd.(n)]
4-element Vector{Int64}:
 3
 5
 7
 9

在內部,述詞會被轉換成BitVector,然後用作索引。

julia> condition = a .< 'D'
26-element BitVector:
 1
 1
 1
 0
 # display truncated

julia> a[condition]
3-element Vector{Char}:
 'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
 'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
 'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 向量運算