Elixir で ニューラルネットワークを学ぶ: パーセプトロン編

# 概要

ニューラルネットワークを学ぶ第一歩として、パーセプトロンの仕組みと実装を簡潔に解説します。
Elixir と Nx を使って、LiveBook 上で動かせるコードとともに進めていきます。

本記事は以下の書籍を参考にしています。

https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E4%BD%9C%E3%82%8BDeep-Learning-%E2%80%95Python%E3%81%A7%E5%AD%A6%E3%81%B6%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0%E3%81%AE%E7%90%86%E8%AB%96%E3%81%A8%E5%AE%9F%E8%A3%85-%E6%96%8E%E8%97%A4-%E5%BA%B7%E6%AF%85/dp/4873117585/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=14MEVKL2MMJH8&dib=eyJ2IjoiMSJ9.oEwjfG16LXrumIaUKlmN2SvuFWfi5_QxX9SOWMqwRhMtsjaABdLCT0A7wfL2obryMuYBTaAl1FiXoTfDFSyuR1ttRSpWm4NMhjKhRyzKQP5SWwCaXlsl3E8PYyOUZIyKtTJcEAHnntDy8lVH__FTgRnU5btFgrSBB4yUK4X-nY9qjLc4YUuLWp0pnmpbXHj-yba53ONlcIi2H92aPDmb1cZJ8VVSw8U80Gk2nLmQgdaZ6Xt8sagE7of3j3SdLJHfV-_S1FlTcbqsKXhM7V62Qaimd6wIPMTN79pnLK7UJNk.ilRcfVnIqP9L23jZuxRaaxBlTn4qwcyF8rnOiQIYiw4&dib_tag=se&keywords=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6+Deep&qid=1748686245&sprefix=%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E5%AD%A6%E3%81%B6+deep%2Caps%2C208&sr=8-1

LiveBook 上で 実装を行いますが、セットアップ方法については、公式サイトをご参照ください。 

https://livebook.dev

また、Nxの関数の詳細な説明は省略します。

# 環境

本記事の内容は、以下の環境で動作確認をしています。

* macOS Sequoia 15.5
* Livebook v0.16.1

# リポジトリ

この記事で紹介したコードは、以下の LiveBook ファイルで試せます。

https://github.com/BlackTanuTanuNinjas/tutorial_neural_network_by_elixir/tree/main/%E3%83%91%E3%83%BC%E3%82%BB%E3%83%97%E3%83%88%E3%83%AD%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90

# 本題

ニューラルネットワークについてGoogle 画像検索で検索すると、入力層、中間層、出力層という単語が載っている画像がヒットします。（ここでは載せませんが）

画像内の⚪︎はニューロンとかノードと呼ばれます。
どの画像もニューロンから伸びた矢印が、中間層の別のニューロンを指しており、最終的に出力層のニューロンに集約するのがわかるかと思います。

入力層のニューロンが中間層のニューロンを指すとき、パーセプトロンという関数を実行した値がその中間層のパーセプトロンが後続の層のニューロンに入力する値となります。

## パーセプトロンとは

パーセプトロンとは、**入力を受け取って出力するかしないかを決める関数**のことです。

計算には<font color="red">**入力**</font>、<font color="red">**重み**</font>、<font color="red">**バイアス**</font>を使用します。

入力と重みを乗算して、バイアスを総和したものが `0` を超えた時に`1`を出力し、超えない時に`0`を出力します。

このように `1` を出力することを <font color="red">**発火**</font> と呼び、重みとバイアスを調整することで発火をコントロールできます。

以下に、単純な論理回路を実装する例を示します。

### 関数の実装

以下はANDゲートという論理回路を実装したものになります。

```elixir
  def and_gate(x1, x2) do
    x = Nx.tensor([x1, x2])
    w = Nx.tensor([0.5, 0.5])
    b = Nx.tensor(-0.7)
    temp =  Nx.add(Nx.dot(x, w), b)

    if Nx.to_number(temp) > 0 do
      1
    else
      0
    end
  end
```

ここでは、重みとバイアスはそれぞれ以下のようになっています。

* 重みは、 `[0.5, 0.5]`
* バイアスを `-0.7` 

この関数に引数として `1, 1` を渡した時、重みと乗算(`Nx.dot/2`)して、バイアスを足した(`Nx.add/2`)とき、結果は以下のようになります。

```
temp =  Nx.add(Nx.dot(x, w), b)
IO.inspect(temp)

#Nx.Tensor<
  f32
  0.30000001192092896
>
```

`0` を超えるので発火するため、`1` を出力することになります。

```elixir
IO.puts Gate.and_gate(1,1)

1
```

線形図に表すと以下のようになります。

<img alt="AND.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/23479cbf-181c-4356-9a35-78c5f461e6a2.png" width="50%" />



### 出力のコントロールの例

先ほどの関数において、重みとバイアスと変更してみます。
ここでは、重みを`[-0.5, -0.5]`、バイアスを`0.7`に設定しました。

```
  def nand_gate(x1,x2) do
    x = Nx.tensor([x1, x2])
    w = Nx.tensor([-0.5, -0.5])
    b = Nx.tensor(0.7)
    temp =  Nx.add(Nx.dot(x, w), b)

    if Nx.to_number(temp) > 0 do
      1
    else
      0
    end
  end
```

すると先ほどは違う出力になりました。

```elixir
IO.puts Gate.nand_gate(1,1)

0
```

このように出力を調整することで、NANDゲートが完成しました。

#### まとめ

パーセプトロンでは、上記のほかORを加えた3つの論理回路が実装できます。

```elixir
defmodule Gate do
  def and_gate(x1, x2) do
    x = Nx.tensor([x1, x2])
    w = Nx.tensor([0.5, 0.5])
    b = Nx.tensor(-0.7)
    temp =  Nx.add(Nx.dot(x, w), b)

    if Nx.to_number(temp) > 0 do
      1
    else
      0
    end
  end

  def or_gate(x1,x2) do
    x = Nx.tensor([x1, x2])
    w = Nx.tensor([0.5, 0.5])
    b = Nx.tensor(-0.3)
    temp = Nx.add(Nx.dot(x, w), b)

    if Nx.to_number(temp) > 0 do
      1
    else
      0
    end
  end

  def nand_gate(x1,x2) do
    x = Nx.tensor([x1, x2])
    w = Nx.tensor([-0.5, -0.5])
    b = Nx.tensor(0.7)
    temp =  Nx.add(Nx.dot(x, w), b)

    if Nx.to_number(temp) > 0 do
      1
    else
      0
    end
  end
end
```

ANDの真理値表は以下のようになります。

| x1 | x2 | 出力 |
|----|----|------|
| 0  | 0  | 0    |
| 0  | 1  | 0    |
| 1  | 0  | 0    |
| 1  | 1  | 1    |

これらの論理回路は、1つの発火で表現できるものになり、線形で表現できるものになります。(AND, OR, NAND)

<img alt="AND.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/23479cbf-181c-4356-9a35-78c5f461e6a2.png" width="25%" />

<img alt="OR.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/cb3118a7-c76a-4145-8e5c-14785ac079a0.png" width="25%" />

<img alt="NAND.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/01c8ae0a-a41b-4f36-8054-4845c7f71093.png" width="25%" />


## 多層パーセプトロン

**多層パーセプトロン**とは、先ほどのパーセプトロンを重ねたものを指します。

以下に、XORの論理回路を実装し、多層パーセプトロンの具体的な構造を解説します

### XORの実装

XORは、単層のパーセプトロンでは表現できない非線形な論理です。

<img alt="XOR.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/a91c3fc6-4d77-40bf-82c9-57e7d6ad83f5.png" width="25%" />

しかし、<font color="red">**パーセプトロンを重ねる（多層にする）**</font> ことで、XOR を実現することが可能になります。  
具体的には、前述の NAND、OR、AND を組み合わせることで、XOR を構築できます。

```elixir
  def xor_gate(x1, x2) do
    s1 = Gate.nand_gate(x1, x2)
    s2 = Gate.or_gate(x1, x2)
    Gate.and_gate(s1, s2)
  end
```

図で表すと、次のように2層構造のパーセプトロンになります。

<img alt="XOR図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3455723/f5576dcd-b284-453e-a417-ae54e97ac459.png" width="50%" />

このように、パーセプトロンを**多層に重ねることで、より複雑な論理や関数**を表現できるようになります。

# まとめ

本記事ではニューラルネットワークの基礎となる、パーセプトロンについて、Elixir の Nx を使って、実装しながら解説しました。

パーセプトロンの要点を以下のようにまとめました。

* パーセプトロンは「<font color="red">**入力を受けて発火するか決める関数**</font>」
* 重み・バイアスを使って出力を制御する
* 適切な重み・バイアスでAND・OR・NANDが実装可能
* 線形分離できないXORは、多層パーセプトロンで実装できる

今回はパーセプトロンの仕組みを深く理解するため、Axonは使用せず、Nxを使って細部まで実装を行いました。
次回は活性化関数について解説しつつ、ここからはAxonを用いた実装に移行していく予定です。