先前看到了 Programmer Competency Matrix,所以就自己幫自己評比一下。

  • Computer Science
    • data structures: level 3
    • algorithms: level 3
    • systems programming: level 2
  • Software Engineering
    • source code version control: level 3
    • build automation: level 2
    • automated testing: level 2
  • Programming
    • problem decomposition: level 3
    • systems decomposition: level 2
    • communication: level 3
    • code organization within a file: level 2
    • code organization across files: level 2
    • source tree organization: level 3
    • code readability: level 3
    • defensive coding: level 2
    • error handling: level 1
    • IDE: level 2
    • API: level 1
    • frameworks: level 3
    • requirements: level 2
    • scripting: level 2
    • database: level 2
  • Experience
    • languages with professional experience: level 1~2 (familiar with OO language with functional patterns)
    • platforms with professional experience: level 2
    • years of professional experience: level 2
    • domain knowledge: level 2
  • Knowledge
    • tool knowledge: level 2
    • languages exposed to: level 2~3 (used Prolog before)
    • codebase knowledge: level 3 (I started a project before)
    • knowledge of upcoming technologies: level 2
    • platform internals: level 1~2 (not sure)
    • books: level 2 (actually used and read books)
    • blogs: level 3 (but not regularly update blog posts)

留言與分享

Name binding 是一個將資料或是程式碼綁定(binding)到識別符(identifiers)的一個過程。一個識別符綁定到一個物件代表這個識別符會參考(reference)某個物件。

Name binding 在程式語言中式相當重要而複雜的一個議題,而它牽涉到作用域(scope)的問題,物件存在於程式碼的位置(語意)及物件的生命周期(時間)。

繼續閱讀

很多人在寫程式的時候會有些壞習慣,如:

1
2
3
4
5
6
7
X = rand(3, 4)
for i = 1:3
for j = 1:4
X[i, j]
...
end
end

可能多數人看以上這段程式碼並沒有什麼特別的感覺,但是如果要維護的時候就會發現你突然不太理解這段程式碼。

有人知道這邊的 3 是什麼意思嗎?嗯…或許可以從上下文猜出來是陣列的列數的意思。

一旦要更改陣列的大小的時候勢必就要更改這些數字,甚至這些數字散落在程式碼的各個角落就會更加頭痛。

這些數字稱為魔術數字(magic numbers),因為沒有人知道他的意義是什麼!

解法一:使用常數

如果這些數字很常被使用到,而且不會在程式中被變更,請使用常數,像:

1
2
3
4
5
6
7
8
9
const ROWS = 3
const COLUMNS = 4
X = rand(ROWS, COLUMNS)
for i = 1:ROW
for j = 1:COLUMNS
X[i, j]
...
end
end

如此,以後要更改陣列大小只需要更改常數即可,也讓程式碼的可讀性上升。

如果你的程式會更改到這些數字,那麼就用變數。

1
2
3
4
5
6
7
8
9
rows = 3
columns = 4
X = rand(rows, columns)
for i = 1:rows
for j = 1:columns
X[i, j]
...
end
end

解法二:動態

如果陣列的大小不是事先知道的,或是需要動態取得,那麼可以用 size

1
2
3
4
5
6
for i = 1:size(X, 1)
for j = 1:size(X, 2)
X[i, j]
...
end
end

如此可以用在未知大小的陣列上。

留言與分享

有在做套件開發的開發者們應該不陌生 @inbounds 這個 macro,在很多現代程式語言中也有。

在存取陣列時,為了安全性與正確性的考量,避免存取到陣列範圍以外的記憶體位置,很多語言都設置了邊界檢查(bounds check)。

邊界檢查會檢查所存取的索引值是否在陣列的範圍內,但是這樣的檢查會有些微的效能損耗,尤其在迴圈內的情況更有可能被累積而放大,關於 Julia 的 邊界檢查可以參考官方文件 Bounds checking

如果可以確定所存取的索引值一定在範圍內,我們就可以把邊界檢查給移除,以加速陣列的存取。如以下範例:

1
2
3
4
A = rand(3, 4)
@inbounds for i = 1:size(A, 1)
println(A[i, :])
end

或是

1
2
3
4
A = rand(3, 4)
for i = 1:size(A, 1)
@inbounds println(A[i, :])
end

@inbounds 會將程式碼區塊中的邊界檢查給移除,可以參考 @inbounds官方文件。使用時必須注意存取的索引值,否則小則存取的值錯誤,大則可能導致程式崩潰。

先養成好的索引習慣,再考慮將效能提升,加入 @inbounds。相關的資訊也紀錄在官方的效能建議中。

留言與分享

剛好看到一些跟編譯器相關的議題,所以來紀錄一下。

在一些語言中會有行內函式(inline function)的設計,使用的話一般會讓程式的效能變好。

最知名應該是 C 跟 C++ 的 inline

Inline function 會在編譯時期直接將函式內容展開到程式碼中,不過展開與否是由編譯器決定的,inline 的標記只是告訴編譯器這個函式可以成為 inline function。

Inline expansion 就是編譯時期會由編譯器執行的一個動作,看起來與 macro expansion 相似,但不同的是 macro expansion 是在前處理(preprocessing)時期做的,會直接展開在原始碼裡頭,而 inline expansion 則是在編譯時期做的,會在呼叫位點(call site)直接展開。

展開後編譯器便可以進行最佳化,執行時,就不需要做函式呼叫,也不會在 function stack 上多配置空間。一般使用在短小的函式上會有好處,在巨大的函式上使用不一定會有好處。然而過多的 inline function 反而可能造成過多的指令快取的消耗,造成反效果。

在 Julia 中,編譯器會自動偵測哪些函式可以被展開,會自動做 inline expansion。一般短小的函式會自動被編譯器判定要 inline,不過也可以由程式設計師自己指定哪些巨大函式可以 inline,可以參考[文件]](https://docs.julialang.org/en/v1.2/base/base/#Base.@inline)。

除了 @inline 以外,還有 @noinline。為了避免過多的 inline 反而傷害效能,也可以標記一些短小的函式不要 inline。

範例:

1
2
3
@inline function bigfunc(x)
...
end

Julia 什麼時候 inline expansion?

我們來實驗看看,以下有兩個函式,foobar。其中讓 foo 為 inline。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
julia> @inline foo(x) = 3x
foo (generic function with 1 method)

julia> bar(x) = foo(x)^2
bar (generic function with 1 method)

julia> bar(5)
225

julia> @code_lowered bar(5)
CodeInfo(
1 ─ %1 = Main.foo(x)
│ %2 = Core.apply_type(Base.Val, 2)
│ %3 = (%2)()
│ %4 = Base.literal_pow(Main.:^, %1, %3)
└── return %4
)

julia> @code_typed bar(5)
CodeInfo(
1 ─ %1 = Base.mul_int(3, x)::Int64
│ %2 = Base.mul_int(%1, %1)::Int64
└── return %2
) => Int64

你會發現在 lower 的階段仍保留有 foo 的函式呼叫的過程,但是到了 typed 的階段就已經剩下計算的部份了。

所以其實在進入 LLVMIR 以前就已經先做完 inline expansion 了。

相關技術:Inline caching

留言與分享

來分享一下最近貢獻開源專案的小心得,雖然我自己貢獻開源專案的次數跟時間不是很多,不過一個 PR 可以產生不少文字跟討論算是值得紀錄一下的。

我自己在 Julia 的一個統計相關的專案上發了一個 PR,希望補齊在標準化上的一些功能,並且可以支援一維的陣列標準化。

繼續閱讀

之前介紹過將值放在參數型別上。

今天來介紹一下如何可以做到類似運算的效果。

1
2
3
4
5
struct A{T}
end

a1 = A{5}()
a2 = A{3}()

在一些應用場景上會希望將參數欄位上的值做運算,例如加總。

這時候我們可以這樣做:

1
2
3
4
5
6
import Base:+

function +(::A{T}, ::A{S}) where {T, S}
x = T + S
return A{x}()
end

如此一來,就可以簡單搞定囉!

1
println(a1 + a2)
1
A{8}()

留言與分享

這篇被選為 NeurIPS 2018 最佳論文,他將連續的概念帶入了神經網路架構中,並且善用以往解微分方程的方法來做逼近,可以做到跟原方法(倒傳遞)一樣好的程度,而參數使用複雜度卻是常數,更短的訓練時間。

核心觀念

概念上來說,就是將神經網路離散的層觀念打破,將他貫通成為連續的層的網路架構。

連續和離散的差別來自於倒傳遞的過程:

$$
\mathbb{y}_{t+1} = \mathbb{y}_t - \eta \nabla \mathcal{L}
$$

其中 $\nabla \mathcal{L}$ 就是梯度的部份,是向量的,然而我們把他簡化成純量來看的話,他不過就是

$$
\frac{d \mathcal{L}}{dt}
$$

廣義上來說,一個函數的微分,如果是離散的版本就會是

$$
\frac{dy}{dt} = \frac{y(t + \Delta) - y(t)}{\Delta}
$$

如此一來,所形成的方程式就會是差分方程,然而連續的版本就是

$$
\frac{dy}{dt} = \lim_{\Delta \rightarrow 0} \frac{y(t + \Delta) - y(t)}{\Delta}
$$

這個所形成的會是微分方程。

從離散到連續

我們可以從離散的版本

$$
\frac{dy}{dt} = \frac{y(t + \Delta) - y(t)}{\Delta}
$$

把他轉成以下的樣貌

$$
y(t + \Delta) = y(t) + \Delta \frac{dy}{dt}
$$

要將他貫通的話,我們就得由從神經網路的基礎開始,如果是一般的前回饋網路(feed-forward network)當中的隱藏層是像下列這個樣子:

$$
h_{t+1} = f(h_t, \theta)
$$

我們可以發現像是 ResNet 這類的網路有 skip connection 的設置,所以跟一般的前回饋網路不同

$$
h_{t+1} = h_t + f(h_t, \theta)
$$

而 RNN 等等有序列概念的模型也有類似的結構,就是會是前一層的結果加上通過 $f$ 運算後的結果,成為下一層的結果。

這樣的形式跟我們前面提到的形式不謀而合

$$
y(t + \Delta) = y(t) + \Delta \frac{dy}{dt}
$$

只要我們把 $\Delta = 1$ 代入,就成了

$$
y(t+1) = y(t) + \frac{dy}{dt}
$$

以下給大家比對一下

$$
h_{t+1} = h_t + f(h_t, \theta) \\
y(t+1) = y(t) + \frac{dy}{dt}
$$

也就是,我們可以讓

$$
\frac{dy}{dt} = f(h_t, \theta)
$$

神奇的事情就發生了!神經網路 $f$ 就可以被我們拿來計算微分 $\frac{dy}{dt}$!

比較精確的說法是,把神經網路的層 $f$ 拿來逼近微分項,或是說梯度。這樣我們後面就可以用數值方法來逼近解。

$$
y(t + \Delta) = y(t) + \Delta \frac{dy}{dt} \\
\downarrow \\
y(t + \Delta) = y(t) + \Delta f(t, h(t), \theta_t)
$$

要拉成連續的還有一個重要的手段,就是將不同的層 $t$ 從離散的變成連續的,所以作者將 $t$ 做了參數化,將他變成 $f$ 的參數之一,如此一來,就可以在任意的層中放入資料做運算。

最重要的概念導出了這樣的式子

$$
h(t) \rightarrow \frac{dy(t)}{dt} = f(h(t), t, \theta) \rightarrow y(t)
$$

神經網路作為一個系統的微分形式

在傳統科學或是工程領域,我們會以微分式來表達以及建構一個系統。

$$
\nu = \frac{dx}{dt} = t + 1
$$

其實在這邊是一樣的道理,整體來說,我們是換成用神經網路去描述一個微分式,其實本質上就是這樣。

原本的層的概念就是用數學函數來建立的,而層與層之間傳遞著計算的結果。

$$
\mathbb{h_1} = \sigma(W_1 \mathbb{x} + \mathbb{b_1}) \\
\mathbb{y} = \sigma(W_2 \mathbb{h_1} + \mathbb{b_2})
$$

然而變成連續之後,我們等於是用神經網路中的層去建立跟描繪微分形式。

$$
\frac{d h(t)}{dt} = \sigma(W(t) \mathbb{x}(t) + \mathbb{b(t)}) \\
\frac{d y(t)}{dt} = \sigma(W(t) \mathbb{h}(t) + \mathbb{b(t)})
$$

是不是跟如出一轍呢?

$$
\frac{dy(t)}{dt} = f(h(t), t, \theta)
$$

向前傳遞解微分式

我們可以來計算看看隱藏層是長什麼樣子的。在隱藏層的微分式中,也是利用隱藏層去計算出來的。

$$
\frac{dh(t)}{dt} = f(h(t), t, \theta)
$$

基本上,我們只要對上式做積分就可以了。

$$
h(t) = \int f(h(t), t, \theta) dt
$$

這是一個怎樣的概念呢?我們可以來看看下圖。

我們做積分這件事其實是用 $h(t_0)$ 來推斷 $h(t_1)$ 的,這跟神經網路的向前傳遞是一樣的行為。

$$
h(t_1) = F(h(t), t, \theta) \bigg|_{t=t_0}
$$

這樣的積分動作,我們可以用 $t_0$ 時間點的資訊來解 $h(t_1)$。

這樣的解法在程式上就會交由 ODE Solver 去處理。

$$
h(t_1) = ODESolve(h(t_0), t_0, t_1, \theta, f)
$$

反向傳遞解函數

$$
\mathcal{L}(t_0, t, \theta) = \mathcal{L}(ODESolve(\cdot))
$$

$$
\frac{\partial \mathcal{L}}{\partial h(t)} = -a(t)
$$

adjoint state

$$
a(t) = \int -a(t)^T \frac{\partial f}{\partial h} dt = - \frac{\partial \mathcal{L}}{\partial h(t)}
$$

$$
a(t) = \int_{t_1}^{t_0} -a(t)^T \frac{\partial f(h(t), t, \theta)}{\partial h(t)} dt
$$

擴充狀態(augmented state)

$\frac{d \theta}{dt} = 0$

$\frac{dt}{dt} = 1$

let $\begin{bmatrix}
h \\
\theta \\
t
\end{bmatrix}$ be a augmented state

augmented state function:

$$
f_{aug}(\begin{bmatrix}
h \\
\theta \\
t
\end{bmatrix}) =
\begin{bmatrix}
f(h(t), t, \theta) \\
0 \\
1
\end{bmatrix}
$$

augmented state dynamics:

$$
\frac{d}{dt}
\begin{bmatrix}
h \\
\theta \\
t
\end{bmatrix}

f_{aug}(
\begin{bmatrix}
h \\
\theta \\
t
\end{bmatrix})
$$

augmented adjoint state:

$$
\begin{bmatrix}
a \\
a_{\theta} \\
a_t
\end{bmatrix}
$$

$a = \frac{\partial \mathcal{L}}{\partial h}$

$a_{\theta} = \frac{\partial \mathcal{L}}{\partial \theta}$

$a_t = \frac{\partial \mathcal{L}}{\partial t}$

$$
\frac{d a_{aug}}{dt} = -
\begin{bmatrix}
a \frac{\partial f}{\partial h} \\
a \frac{\partial f}{\partial \theta} \\
a \frac{\partial f}{\partial t}
\end{bmatrix}
$$

留言與分享

Yueh-Hua Tu

目標是計算生物學家!
Systems Biology, Computational Biology, Machine Learning
Julia Taiwan 發起人


研發替代役研究助理


Taiwan