ゼロから作る Deep LearningをElixirで学ぶシリーズ ~ 3章 Neural Network : NN を学ぶ （前編）~

東京にいるけどfukuokaexのYOSUKEです。
最近、[エリクサーちゃんで学ぶ Elixir](https://youtu.be/_QzrB6tTPwA)の動画を作成し始めてるので良かったらチャンネル登録お願いします。

ゼロから作る Deep Learningという本を見ながら Elixirの計算ライブラリ系を学ぶシリーズです。

## 学習開始
この書籍を元にElixirで学びます。

<a target="_blank" href="https://www.amazon.co.jp/%25E3%2582%25BC%25E3%2583%25AD%25E3%2581%258B%25E3%2582%2589%25E4%25BD%259C%25E3%2582%258BDeep-Learning-%25E2%2580%2595Python%25E3%2581%25A7%25E5%25AD%25A6%25E3%2581%25B6%25E3%2583%2587%25E3%2582%25A3%25E3%2583%25BC%25E3%2583%2597%25E3%2583%25A9%25E3%2583%25BC%25E3%2583%258B%25E3%2583%25B3%25E3%2582%25B0%25E3%2581%25AE%25E7%2590%2586%25E8%25AB%2596%25E3%2581%25A8%25E5%25AE%259F%25E8%25A3%2585-%25E6%2596%258E%25E8%2597%25A4-%25E5%25BA%25B7%25E6%25AF%2585/dp/4873117585?&_encoding=UTF8&tag=edtechfather-22&linkCode=ur2&linkId=5100d6b10a6078f983516dfc20f685cc&camp=247&creative=1211">ゼロから作るDeep Learning</a> ―Pythonで学ぶディープラーニングの理論と実装

今回は、第3章のニューラルネットワークについて学びます。
この3章ですが、量が多いので前編と後編に分けてアウトプットします。
これは個人的な学習の備忘録でもあるので、書籍に解説された事と自分で調べた事を記載して行きます。解釈が違うなどあるかもなので、これみて学習ではなく、必ず書籍を見て学習することをお勧めします。 

ちなみに今までの内容はこちら
ゼロから作る Deep Learning をElixirで学ぶシリーズ
|> [~ Numpy -> Nx に置き換えて 1章やる~](https://qiita.com/Yoosuke/items/8599ed0a976de1371ead)
|> [~ Perceptron（パーセプトロンの実装） ~](https://qiita.com/Yoosuke/items/751a9fe05b89755e25a8)

## 3章 ニューラルネットワーク

#### ニューラルネットワークとは
Wikiによると、入力を線形変換する処理単位」がネットワーク状に結合した数理モデルと書かれていました。書籍では、ニューラルネットワークを2章で学んだパーセプトロンとの違いから解説を試みてくれています。

### 3.1 パーセプトロンからニューラルネットワークへ
この辺りは詳しくは書籍に任せるとして、パーセプトロンの計算式のバイアスを入力に加えて式を変形し、以下の式をもとに話しが進んでいます。

* a = b + w1x1 + w2x2
* y = h(a)

この時、aは入力信号の総和を表し、yは出力を表し、h()は活性化関数を表す。そして、この活性化関数がパーセプトロンからニューラルネットワークへ進むための架け橋になるようです。(書籍 p44)

## 3.2 活性化関数

* h(x) = { x <= 0 -> 0, x > 0 -> 1 }  

h(x)は閾値を境にして出力が切り替わる関数で、「ステップ関数」ともいう。要するにパーセプトロンではこのステップ関数を利用していた。このステップ関数を別の関数に変える事でパーセプトロンのモデルがニューラルネットワークモデルに切り替わる。

と言うことで、ニューラルネットワークで利用される関数の紹介

### 3.2.1 シグモイド関数

* h(x) = 1 / 1 + exp(-x)

exp(-x)は e の -x 乗 を意味し、eはネイピア数(2.7182 ....) ネイピア数ってなに？と調べたら自然体数の底だった。

* (1 + 1 / n)のn乗 にした時、 n -> ∞ にしていくと、e(2.7182 ...)に収束していく

シグモイド関数とは、あらゆる入力値を0.0～1.0の範囲の数値に変換して出力する関数となる。
と言うことで、シグモイド関数とステップ関数の違いを視覚的に理解する為にまずはステップ関数を実装していきましょう。

### 3.2.2 ステップ関数の実装

```elixir
defmodule Activator do
  
  def step(x_list) when is_list(x_list) do
    broadcast(x_list)
    |> Nx.to_flat_list
    |> Enum.map(&step(&1))
  end

  def step(x) do
    case x do
      x when x <= 0 -> 0
      x when x > 0 -> 1
    end
  end

  defp broadcast(array) do
    Nx.tensor(array)
    |> Nx.broadcast({array |> length})
  end

end
```

ここで、前回作った ```arrange```関数を改良して```arrange(start, stop, pitch)```
を3つの引数に対応したものにします。

```elixir
defmodule Nxnp do

  def arrange(start, stop, pitch) do
    string_float = Float.to_string(stop)
    <<head,46,tail::binary>> = string_float
    s = <<head>> |> String.to_integer

    num = String.codepoints(tail) |> length()
    point_list  = Enum.map(1..num, fn _ -> pitch end)
    digit = Enum.reduce(point_list, fn x, acc -> x * acc end)

    e = String.to_integer(tail)
    finish = (s * 10) + e
    {begin, _} = start / pitch |> Float.to_string() |> Integer.parse()

    Enum.map(begin..finish, fn x -> Float.round(x * digit, num) end)
  end

end
```

#### 実行

```elixir
x = Nxnp.arrange(-5.0, 5.0, 0.1)
y = Activator.step(x)

alias Expyplot.Plot
Plot.ylim([0.1, 1.1])
Plot.plot([x, y])
Plot.show()

```

![step関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72962/355bcc2c-d57e-4231-13c9-a87a15b1f80c.png)


### 3.2.4 シグモイド関数の実装

```elixir
defmodule Activator do
# 省略
  def sigmoid(x) when is_number(x) do
    ans = x
    |> Nx.multiply(-1)
    |> Nx.exp()

    Nx.divide(1, Nx.add(1,ans))
  end

  def sigmoid(x) when is_list(x) do
      ans = broadcast(x)
      |> Nx.multiply(-1)
      |> Nx.exp()

      Nx.divide(1, Nx.add(1, ans))
  end

  defp broadcast(array) do
    Nx.tensor(array)
    |> Nx.broadcast({array |> length})
  end

end
```

実行

```elixir
iex()> x = Nxnp.arrange(-5.0, 5.0, 0.1)
iex()> y = Activator.sigmoid(x)
iex()> alias Expyplot.Plot
iex()> Plot.plot([x, Nx.to_flat_list(y)])
iex()> Plot.ylim([-0.1, 1.1])
iex()> Plot.show()
```

![sigmoid関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72962/ff795d40-2f8a-6f6c-acd8-4d395938f36b.png)

### 3.2.5 シグモイド関数とステップ関数の比較

```elixir
iex()> x = Nxnp.arrange(-5.0, 5.0, 0.1)                             
[-5.0, -4.9, -4.8, -4.7, -4.6, -4.5, -4.4, -4.3, -4.2, -4.1, -4.0, -3.9, -3.8,
 -3.7, -3.6, -3.5, -3.4, -3.3, -3.2, -3.1, -3.0, -2.9, -2.8, -2.7, -2.6, -2.5,
 -2.4, -2.3, -2.2, -2.1, -2.0, -1.9, -1.8, -1.7, -1.6, -1.5, -1.4, -1.3, -1.2,
 -1.1, -1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, ...]
iex()> y2 = Activator.sigmoid(x)                                    
#Nx.Tensor<
  f32[101]
  [0.006692850962281227, 0.0073915403336286545, 0.00816257018595934, 0.009013300761580467, 0.009951802901923656, 0.010986942797899246, 0.012128434143960476, 0.013386915437877178, 0.014774034731090069, 0.016302499920129776, 0.01798621006309986, 0.0198403038084507, 0.02188127115368843, 0.024127019569277763, 0.026596995070576668, 0.02931223064661026, 0.03229546174407005, 0.03557119145989418, 0.03916572034358978, 0.04310726001858711, 0.04742587357759476, 0.052153561264276505, 0.05732417479157448, 0.06297335773706436, 0.06913843005895615, 0.07585817575454712, 0.08317269384860992, 0.09112296253442764, 0.09975048154592514, 0.10909683257341385, 0.11920291930437088, 0.13010847568511963, 0.14185106754302979, 0.15446525812149048, 0.1679816097021103, 0.18242552876472473, 0.19781610369682312, 0.21416503190994263, 0.23147521913051605, 0.2497398853302002, 0.2689414322376251, 0.28905048966407776, 0.31002551317214966, 0.3318122327327728, 0.3543437123298645, 0.3775406777858734, 0.40131235122680664, 0.4255574941635132, 0.4501660168170929, 0.47502079606056213, ...]
>
iex()> y1 = Activator.step(x)
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
iex()> alias Expyplot.Plot                                          
Expyplot.Plot
iex()> Plot.plot([x, Nx.to_flat_list(y2)], label: "sigmoid function")
"[<matplotlib.lines.Line2D object at 0x126381060>]"
iex()> Plot.plot([x, y1], label: "step function")                    
"[<matplotlib.lines.Line2D object at 0x126383340>]"
iex()> Plot.ylim([-0.1, 1.1])                                        
"(-0.1, 1.1)"
iex()> Plot.title("sigmoid & step")
"Text(0.5, 1.0, 'sigmoid & step')"
iex()> Plot.legend
"Legend"
iex()> Plot.show()
```

![sigmoid&step.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72962/2ccf7a9b-fc1e-bad6-520f-4e50e4c4dfe9.png)

* シグモイド関数とステップ関数の相違点
    * シグモイド関数の方が滑らか
    * ステップ関数は0か１かを返す、シグモイドは実数を返す
* シグモイド関数とステップ関数の類似点
    * 入力情報が重要なら大きな値を返し、重要でなければ小さな値を返す
    * どんなに値が大きくても、出力は0から１の範囲内で返す
    * シグモイド関数とステップ関数どちらも非線形関数である

### 3.2.6 非線形関数

ここは詳しくは書籍を見てください。端的に言うと、ニューラルネットワーク（NN）の活性化関数には非線形関数を使うことが重要という説明がされてます。

### 3.2.7 ReLU関数

シグモイド関数は古くから使われてきたNNの活性化関数でしたが、最近ではReLU関数(Rectified Linear Unit）という関数が用いられているそうです。

ReLUとは入力が0を超えていればそのまま出力し、0以下なら0を返す関数です。

```elixir
defmodule Activator do
# 省略
  def relu(x) when is_number(x) do
    Nx.max(0, x)
    |> Nx.to_number()
  end

  def relu(x) when is_list(x) do
    ans = broadcast(x)
    |> Nx.to_flat_list()
    |> Enum.map(&relu(&1))
  end

  defp broadcast(array) do
    Nx.tensor(array)
    |> Nx.broadcast({array |> length})
  end
end
```

今回、```Nx.max/2```という関数を利用して入力値が大きい値を返す形式を取りました。

実行
```elixir
iex()> Plot.plot([x, y3], label: "ReLu function")
iex()> Plot.show()
```

![ReLu関数.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72962/9ce76895-d173-e74e-d857-7604ae01f412.png)


## 3.3 多次元配列の計算

多次元配列の計算については、こちらでもやっているので割愛しつつ、まだやってない箇所だけやります。

https://qiita.com/Yoosuke/items/193619975341e560a8f9

https://qiita.com/Yoosuke/items/a961067b17e20240cea3

### 3.3.3 ニューラルネットワークの行列の積

簡単なニューラルネットワークの実装をしていきます。
今回は、バイアスと活性化関数は省略したものを試していきます。
入力x,重みw、出力yの形状で、行列の積を利用します。その為、XとWの次元の一致に気をつけます。

```elixir
iex()> x = Nx.tensor([1, 2]) 
#Nx.Tensor<
  s64[2]
  [1, 2]
>
iex()> Nx.shape x
{2}
iex(98)> w = Nx.tensor([[1,3,5],[2,3,4]])
#Nx.Tensor<
  s64[2][3]
  [
    [1, 3, 5],
    [2, 3, 4]
  ]
>
iex(99)> Nx.shape w
{2, 3}
iex(100)> Nx.dot(x, w)
#Nx.Tensor<
  s64[3]
  [5, 9, 13]
>
iex(101)> y = Nx.dot(x, w) |> Nx.to_flat_list
[5, 9, 13]
```

行列の積を使う事で、yの計算がシンプルに実装できるのが利点です。

３章は長いので、次は３層のニューラルネットワークの実装をしていきたいと思います。
今回はここまで