🗃

Math&Julia #01|人工ニューロン » 論理ゲートを再現

に公開

はじめに

1個の人工ニューロンを実装して、これに適切な重みを設定することによって論理ゲートを再現します。本来であれば、この設定に必要な重みを探索するのに数理最適化を行いますが、今回は手作業で、ニューロンの反応を見ながら値を少しずつ変えることによって探索しました。

ニューロンのモデル


図 1 ニューロンと論理ゲート

総入力の計算

今回のニューロンのモデルは、重みw_nとバイアスbを統一的に扱っています。これを、総入力を求める加重和の計算式に反映させます。

\tag{1} u=w_1x_1+w_2x_2+b\cdot 1

上述の統一的な扱いによるメリットとして、数式全体を一括してベクトルの内積に変形できます。

\tag{2} u=\begin{pmatrix}w_1\\w_2\\b\end{pmatrix}\cdot \begin{pmatrix}x_1 \\x_2\\1\end{pmatrix}= \begin{pmatrix}w_1  w_2 b\end{pmatrix} \begin{pmatrix}x_1\\x_2\\1\end{pmatrix}

2つのベクトルをそれぞれ\bold{w},\bold{x}に置き換えると、実装レベルの数式になります。

\tag{3} u=\bold{w}\cdot\bold{x}=\bold{w}^\mathsf{T}\bold{x}

数式のテスト

数式の変形に問題が無いことを確認します。

Julia
using LinearAlgebra

w = [2, 3, 4]
x = [3, 2, 1]

u = w[1]x[1] + w[2]x[2] + w[3]x[3]      # 数式(1)のテスト
println(u)

u = w ⋅ x                               # 数式(3)のテスト 1/2
println(u)

u = w'x                                 # 数式(3)のテスト 2/2(wを転置)
println(u)
実行結果
16
16
16

実装

ニューロンを 1個だけ使う場合は、再現できる論理ゲートの種類に制限があります。今回はANDとORの 2つの論理ゲートを再現させることにしました。XORについては線形分離できないので再現できません。XORを再現させるには、ニューロンを複数配置してニューラルネットワークを構成する必要があります。

実装は、先に考え方の分かりやすいベクトル版からはじめて、次にこれを行列版に書き換えることにします。

ベクトル版

総入力の計算はpredict関数に実装しました。この総入力の計算結果をsigmoid関数に通すことによって0.0〜1.0(0%〜100%)までの確率的な値に変換できます。今回は二値分類によって論理ゲートを再現したかったので、sigmoid関数の出力を更にround関数に通して二値化{0, 1}しました。

Julia
using Printf

sigmoid(x)  = 1 / (1 + exp(-x))
check(flag) = flag ? "✓ PASS" : "✗ FAIL"

# ニューロン
struct Neuron
    w::Vector{Float64}          # 重み
end

# 予測
function predict(n::Neuron, X, T)
    function debug(x, u, y, ŷ, t)
        su = @sprintf("%+.2f", u)
        sy = @sprintf("%.3f",  y)
        sc = check(ŷ == t)
        println("x1=$(x[1]), x2=$(x[2]), u=$su, y=$sy, ŷ=$ŷ, $sc")
    end
    result::Vector{Int} = []
    for (x, t) in zip(X, T)
        u = n.w'x               # 総入力
        y = sigmoid(u)          # 活性化関数
        ŷ = round(Int, y)       # 二値化
        push!(result, ŷ)        # 予測値を集約
        debug(x, u, y, ŷ, t)
    end
    result
end

# 重み(手作業で探索した結果)
w_and = [0.7, 0.7, -0.9]        # AND
w_or  = [0.9, 0.9, -0.5]        # OR

# 入力値
X = [ [0, 0, 1],
      [0, 1, 1],
      [1, 0, 1],
      [1, 1, 1] ]

# 正解値
T_and = [0, 0, 0, 1]            # AND
T_or  = [0, 1, 1, 1]            # OR

println("ANDを再現")
n = Neuron(w_and)
Ŷ = predict(n, X, T_and)
println("結果: $Ŷ")

println("\nORを再現")
n = Neuron(w_or)
Ŷ = predict(n, X, T_or)
println("結果: $Ŷ")
実行結果
ANDを再現
x1=0, x2=0, u=-0.90, y=0.289, ŷ=0, ✓ PASS
x1=0, x2=1, u=-0.20, y=0.450, ŷ=0, ✓ PASS
x1=1, x2=0, u=-0.20, y=0.450, ŷ=0, ✓ PASS
x1=1, x2=1, u=+0.50, y=0.622, ŷ=1, ✓ PASS
結果: [0, 0, 0, 1]

ORを再現
x1=0, x2=0, u=-0.50, y=0.378, ŷ=0, ✓ PASS
x1=0, x2=1, u=+0.40, y=0.599, ŷ=1, ✓ PASS
x1=1, x2=0, u=+0.40, y=0.599, ŷ=1, ✓ PASS
x1=1, x2=1, u=+1.30, y=0.786, ŷ=1, ✓ PASS
結果: [0, 1, 1, 1]

行列版

論理ゲートを再現する程度であればベクトル版のままでも良いのですが、この機会に、より実践的な行列の使い方を習得したいと思います。ベクトル版から行列版への変更は次のようにします。

  • 入力値:ベクトルを4×3の行列に変更する。
  • 重み :ベクトルを3×1の行列に変更する。
  • 加重和:ベクトルの内積(\bold{w}^\mathsf{T}\bold{x})を行列の積(XW)に変更する。
  • predict関数の15行のコードを1行に書き換える。それには、
    • 加重和に対して、sigmoid関数とround関数を連鎖的にブロードキャストする。
    • 入れ子で定義したdebug関数を削除する。
Julia
sigmoid(x)  = 1 / (1 + exp(-x))

# ニューロン(行列版)
struct Neuron_matrix
    W::Matrix{Float64}              # 重み
end

# 予測
function predict(n::Neuron_matrix, X)
    round.(Int, sigmoid.(X*n.W))
end

# 重み(手作業で探索した結果)
W_and = [ 0.7; 0.7; -0.9;; ]        # AND(3×1の行列)
W_or  = [ 0.9; 0.9; -0.5;; ]        # OR (3×1の行列)

# 入力値
X = [ 0 0 1; 0 1 1; 1 0 1; 1 1 1 ]  # 4×3の行列

println("ANDを再現")
n = Neuron_matrix(W_and)
Ŷ = predict(n, X)
println("結果: $Ŷ")

println("\nORを再現")
n = Neuron_matrix(W_or)
Ŷ = predict(n, X)
println("結果: $Ŷ")
実行結果
ANDを再現
結果: [0; 0; 0; 1;;]

ORを再現
結果: [0; 1; 1; 1;;]

Discussion