🥅

Julia言語でニューラルネットワークを理解する

2025/01/15に公開

はじめに

Julia言語は自然科学とも機械学習とも相性が良く, それらの組み合わせにおいて絶大な威力を発揮します. ここではJulia言語の機械学習パッケージと数式処理システムを組み合わせることで, ブラックボックスとして扱われがちなニューラルネットワーク(neural network)の関数形を明示的に書き下して理解していきましょう.

パッケージ

下記のパッケージをインストールしておきます.

パッケージのインストール
import Pkg
Pkg.add("Symbolics")
Pkg.add("CairoMakie")
Pkg.add("Flux")
Pkg.add("Lux")

以降はJulia言語の機械学習パッケージであるFlux.jlLux.jlそれぞれを用いて機械学習モデル(ここではニューラルネットワーク)を構築し, Symbolics.jlで作成したオブジェクトを代入することで, その関数形を確認します. Flux.jlとLux.jlについてはこちらの記事を, Symbolics.jlについてはこちらの記事を参照してください.

Symbolic.jlの使い方

Symbolics.jl@variables x のように作成したオブジェクト x の計算結果をLaTeXで綺麗に表示してくれます. この x をニューラルネットワークに代入すれば, その関数形をLaTeXで表示できます.

Symbolics.jlの使い方
using Symbolics
@variables x
simplify(exp(x) * exp(2x))
\begin{equation} e^{3 x} \end{equation}

Flux.jlによるニューラルネットワークの構築とその関数形の確認

永井 佑紀 著『Juliaではじめる数値計算入門』(技術評論社, 2024) 5-5. ニューラルネットワークによる関数近似 のコードを参考に, 次のような隠れ層1層の全結合型(fully-connected)ニューラルネットワークを構築します.

ここでは関数形をシンプルにするために活性化関数(activation function)に正弦関数sinを採用しました. 普通はsinではなくFlux.reluFlux.sigmoidなどが使われます.

Flux.jlによるニューラルネットワークの構築とその関数形の確認
# Flux.jlの読み込み
using Flux

# Flux.jlによるモデルの構築
model = Flux.Chain(
    x -> [x],              # 引数は1行n列の行列が前提なので実数から変換しておく
    Flux.Dense(1, 2, sin), # 入力層 -> 隠れ層 (2ユニット)
    Flux.Dense(2, 1),      # 隠れ層 -> 出力層
    x -> sum(x)            # 1行n列の行列が出力されるので実数に直す
)

# 関数形の確認
model(x)
\begin{equation} - 0.28759 \sin\left( - 0.86971 x \right) - 0.35904 \sin\left( - 1.1822 x \right) \end{equation}

ニューラルネットワークの関数形が確認できましたが, ウェイトの初期値は乱数によって決定されており, バイアスの初期値は0になっていることに注意してください.

バイアスの確認方法
model[2].bias
# 2-element Vector{Float32}:
#  0.0
#  0.0

ウェイトとバイアスを次のように指定すると関数形を確認しやすくなります. 例えば, ウェイトは行列なので層と行と列の3桁の数字で w^{(3)}_{23}323w^{(2)}_{12}212 と表します. バイアスはベクトルなので2桁の数字で b^{(3)}_{1}31 と表します.

ウェイトとバイアスの指定
model[2].weight[1,1] = 211
model[2].weight[2,1] = 221
model[2].bias[1]     = 21
model[2].bias[2]     = 22
model[3].weight[1,1] = 311
model[3].weight[1,2] = 312
model[3].bias[1]     = 31
model(x)
\begin{equation} 31 + 311 \sin\left( 21 + 211 x \right) + 312 \sin\left( 22 + 221 x \right) \end{equation}

富谷 昭夫, 橋本 幸士, 金子 隆威, 瀧 雅人, 広野 雄士, 唐木 田亮, 三内 顕義 著, 橋本 幸士 編『学習物理学入門』(朝倉書店, 2024) A2. ニューラルネットワーク(NN)の記号に合わせると, 次のような関数形であることが確認できています.

f_\theta(x) = w^{(3)}_{11} \sigma(w^{(2)}_{11} z^{(1)} + b^{(2)}_1) + w^{(3)}_{12} \sigma(w^{(2)}_{21} z^{(1)} + b^{(2)}_2) + b^{(3)}_1

ただし \sigma(x) = \sin(x), z^{(1)} = x, \theta = \{ w^{(2)}_{11}, w^{(2)}_{21}, b^{(2)}_1, b^{(2)}_2, w^{(3)}_{12}, w^{(3)}_{11}, b^{(3)}_1 \} です. この関数の具体的な値は次のように計算できます.

ニューラルネットワークの具体的な値
@show model(0.0)
@show model(0.5)
@show model(1.0)
# model(0.0) = 288.4383f0
# model(0.5) = 425.71756f0
# model(1.0) = -389.70456f0

電卓で検算してみましょう.

\begin{aligned} f_\theta(0) &= 311 \sin(21) + 312 \sin(22) + 31 \\ &= 311 \times 0.8366556385360561 + 312 \times -0.008851309290403876 + 31 \\ &= 288.4382950861074 \end{aligned}

確かに合っています. Juliaだと次のように検算できます.

検算
x = 0
311 * sin(211*x+21) + 312 * sin(221*x+22) + 31
# 288.4382950861074

当然ながら, 次のようにグラフとして書くこともできます. ただし, 上記とはパラメータが違うので注意してください. ここでは f_\theta(x) = \sin(x) となるようにパラメータを指定しています.

ニューラルネットワークのプロット
# ニューラルネットワークの構築
using Flux
model = Flux.Chain(
    x -> [x],              # 引数は1行n列の行列が前提なので実数から変換しておく
    Flux.Dense(1, 2, sin), # 入力層 -> 隠れ層 (2ユニット)
    Flux.Dense(2, 1),      # 隠れ層 -> 出力層
    x -> sum(x)            # 1行n列の行列が出力されるので実数に直す
)

# ウェイトとバイアスの指定
model[2].weight[1,1] = 1.0
model[2].weight[2,1] = 0.0
model[2].bias[1]     = 0.0
model[2].bias[2]     = 0.0
model[3].weight[1,1] = 1.0
model[3].weight[1,2] = 0.0
model[3].bias[1]     = 0.0

# プロット
using CairoMakie
fig = Figure(size=(420,300), fontsize=11.5, backgroundcolor=:transparent)
axis = Axis(fig[1,1], xlabel=L"$x$", ylabel=L"$y$", xlabelsize=16.5, ylabelsize=16.5)
lines!(axis, 0..5, x -> model(x), label="model")
fig

入力や出力をベクトルにしたり, 各層のユニットを増やすこともできます. もはやブラックボックスとして扱った方が楽ですが, 具体的な数式として書き下すこともできるという実感が重要です.

より複雑な例
# Symbolics.jlによるオブジェクトの作成
using Symbolics
@variables x₁, x₂

# Flux.jlによるニューラルネットワークの構築
using Flux
model = Flux.Chain(
    Flux.Dense(2, 4, sin), # 入力層 -> 隠れ層 (4ユニット)
    Flux.Dense(4, 4, sin), # 隠れ層 -> 隠れ層 (4ユニット)
    Flux.Dense(4, 2),      # 隠れ層 -> 出力層
)

# ウェイトとバイアスの指定
for i in 1:length(model)
    for k in keys(model[i].weight)
        model[i].weight[k] = 100i + 10k[1]+ k[2]
    end
    for j in keys(model[i].bias)
        model[i].bias[j] = 10i + j
    end
end

# 関数形の確認
model([x₁, x₂])
\begin{equation} \left[ \begin{array}{c} 31 + 314 \sin\left( 24 + 241 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 244 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 243 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 242 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 311 \sin\left( 21 + 211 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 214 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 213 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 212 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 312 \sin\left( 22 + 221 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 224 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 223 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 222 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 313 \sin\left( 23 + 231 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 234 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 233 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 232 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) \\ 32 + 324 \sin\left( 24 + 241 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 244 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 243 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 242 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 321 \sin\left( 21 + 211 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 214 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 213 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 212 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 322 \sin\left( 22 + 221 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 224 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 223 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 222 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) + 323 \sin\left( 23 + 231 \sin\left( 11 + 111 \mathtt{x{_1}} + 112 \mathtt{x{_2}} \right) + 234 \sin\left( 14 + 141 \mathtt{x{_1}} + 142 \mathtt{x{_2}} \right) + 233 \sin\left( 13 + 131 \mathtt{x{_1}} + 132 \mathtt{x{_2}} \right) + 232 \sin\left( 12 + 121 \mathtt{x{_1}} + 122 \mathtt{x{_2}} \right) \right) \\ \end{array} \right] \end{equation}

Lux.jlによるニューラルネットワークの構築とその関数形の確認

Flux.jlからLux.jlへの移植方法についてはこちらの記事を参照してください. 同じ結果が得られます.

Lux.jlによるニューラルネットワークの構築とその関数形の確認
# Lux.jlの読み込み
using Lux
using Random

# Flux.jlによるニューラルネットワークの構築
model = Lux.Chain(
    x -> [x],
    Lux.Dense(1 => 2, sin),
    Lux.Dense(2 => 1),
    x -> sum(x),
)

# ウェイトとバイアスの指定
rng = Random.MersenneTwister(123)
ps, st = Lux.setup(rng, model)
ps = (
    layer_1 = NamedTuple(),
    layer_2 = (weight = [211; 221;;], bias = [221, 222]),
    layer_3 = (weight = [311 312], bias = [321]),
    layer_4 = NamedTuple()
)

# 関数形の確認
using Symbolics
@variables x
first(model(x, ps, st))
\begin{equation} 31 + 311 \sin\left( 21 + 211 x \right) + 312 \sin\left( 22 + 221 x \right) \end{equation}

まとめ

Flux.jlLux.jlそれぞれを用いてニューラルネットワークを構築し, Symbolics.jlで作成したオブジェクトを代入することで, その関数形を確認しました. 関数形を明示的に書き下すことによってニューラルネットワークについての理解が深まったのではないでしょうか.

バージョン情報

バージョン情報
Julia v1.11.2
Symbolics v6.22.1
CairoMakie v0.12.18
Flux v0.16.0
Lux v1.4.4

参考文献

https://gist.github.com/ohno/62e9c3ff71f1836bd300a41a368b00e1

Discussion