LoginSignup
8
5

More than 1 year has passed since last update.

ゼロから作る Deep LearningをElixirで学ぶシリーズ ~ 3章 Neural Network : NN を学ぶ (後編 ①)~

Posted at

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

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

学習開始

この書籍を元にElixirで学びます。

ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装

今回は、第3章のニューラルネットワークについて学ぶ後編です。

前回はこちら

ちなみに今までの内容はこちら
ゼロから作る Deep Learning をElixirで学ぶシリーズ
|> ~ Numpy -> Nx に置き換えて 1章やる~
|> ~ Perceptron(パーセプトロンの実装) ~
|> ~ 3章 Neural Network : NN を学ぶ (前編)~

3.4.2 各層における信号伝達の実装

ここも、詳細は書籍を確認してもらうとして、3層構造の実装をしてみます。
xは入力、wは重み、bはバイアス

x = Nx.tensor([[1.0, 0.5]])
w1 = Nx.tensor([[0.1, 0.3, 0.5],[0.2,0.4,0.6]])
b1 = Nx.tensor([0.1, 0.2, 0.3])
Nx.shape x
Nx.shape w1
Nx.shape b1
a1 = Nx.dot(x, w1) |> Nx.add(b1)
z1 = Activator.sigmoid(Nx.to_flat_list(a1))

w2 = Nx.tensor([[0.1,0.4],[0.2, 0.5],[0.3, 0.6]])
b2 = Nx.tensor([0.1, 0.2])
Nx.shape z1
Nx.shape w2
Nx.shape b2
a2 = Nx.dot(z1, w2) |> Nx.add(b2)
z2 = Activator.sigmoid(Nx.to_flat_list(a2))

w3 = Nx.tensor([[0.1, 0.3], [0.2, 0.4]])
b3 = Nx.tensor([0.1, 0.2])
a3 = Nx.dot(z2, w3) |> Nx.add(b3)

最後は、恒等関数を出力層への活性化関数として利用します。そこで、恒等化関数を定義します。
恒等関数は受け取った値をそのまま返すだけです。

def identity_function(x) do
     x
end

ここまでのを基にinit_network()forward()という関数を定義していきます。

defmodule Activator do
  def init_network() do
    w1 = Nx.tensor([[0.1, 0.3, 0.5], [0.2, 0.4, 0.6]])
    b1 = Nx.tensor([0.1, 0.2, 0.3])
    w2 = Nx.tensor([[0.1, 0.4], [0.2, 0.5], [0.3, 0.6]])
    b2 = Nx.tensor([0.1, 0.2])
    w3 = Nx.tensor([[0.1, 0.3], [0.2, 0.4]])
    b3 = Nx.tensor([0.1, 0.2])
    [w1, b1, w2, b2, w3, b3]
  end

  def forward(network, x) do
    [w1, b1, w2, b2, w3, b3] = network

    a1 = Nx.dot(x, w1) |> Nx.add(b1)
    z1 = sigmoid(Nx.to_flat_list(a1))

    a2 = Nx.dot(z1, w2) |> Nx.add(b2)
    z2 = sigmoid(Nx.to_flat_list(a2))

    a3 = Nx.dot(z2, w3) |> Nx.add(b3)

    identity_function(Nx.to_flat_list(a3))
  end

  def identity_function(x) do
    x
  end

end
network = Activator.init_network()
x = Nx.tensor([1.0, 0.5])
y = Activator.forward(network, x)

3.5 出力層の設計

学習メモ:

  • ニューラルネットワークは分類問題と回帰問題の両方に用いることができる。
    • 分類問題とは:データがどんクラスに属性するか? を分類する問題
      • 例:人の写った写真から男性か女性かを分類するような問題
    • 回帰問題とは:ある入力データから連続的数値の予測を行う問題
      • 例:人の写った画像からその人の体重を予測するような問題
  • 上記分類問題を分別するためには、ニューラルネットワークの出力層の活性化関数を変更する事で実現できる。
    • 回帰問題: NNの出力層の活性化関数を恒等関数にする
    • 分類問題: NNの出力層の活性化関数をソフトマックス関数にする

3.5.1 恒等関数とソフトマックス関数

恒等関数

  • 恒等関数は3.4.2で説明済み

ソフトマックス関数の実装

  • ソフトマックス関数の関数の数式
    softmap_func.png
iex()> a = Nx.tensor([0.3, 2.9, 4.0])
#Nx.Tensor<
  f32[3]
  [0.30000001192092896, 2.9000000953674316, 4.0]
>
iex()> exp_a = Nx.exp(a)
#Nx.Tensor<
  f32[3]
  [1.3498588800430298, 18.17414665222168, 54.598148345947266]
>
iex()> sum_exp_a = Nx.sum(exp_a)
#Nx.Tensor<
  f32
  74.12215423583984
>
iex()> sum_exp_a |> Nx.to_number
74.12215423583984
iex()> y = Nx.divide exp_a, sum_exp_a
#Nx.Tensor<
  f32[3]
  [0.018211273476481438, 0.24519182741641998, 0.7365968823432922]
>
iex()> recompile
Compiling 1 file (.ex)
warning: variable "ans" is unused (if the variable is not meant to be used, prefix it with an underscore)
  lib/activator.ex:36: Activator.relu/1

:ok
iex()> Activator.softmax([0.3, 2.9, 4.0])
#Nx.Tensor<
  f32[3]
  [0.0416666679084301, 0.402777761220932, 0.555555522441864]
>

ソフトマックス関数実装

上記を経て、関数化

  def softmax(a) do
    exp_a = Nx.tensor(a) |> Nx.exp()
    sum_exp_a = Nx.sum(exp_a)
    Nx.divide(exp_a, sum_exp_a)
  end

3.5.2 ソフトマックス関数の実装上の注意

詳しくは、書籍参照 : 端的にいうとソフトマックス関数のオーバーフロー対策してね。(p.68)

4c2537e69674a06b7377f298fbcf1152_90_black.png

定数の足し引きをしても結果は変わらない。を利用して、オーバーフロー対策として、入力データの最大値を利用して引くことで正しく計算できるように変換している。

  def softmax(a) do
    tensor_a = Nx.tensor(a)
    c = tensor_a |> Nx.flatten |> Nx.to_flat_list() |> Enum.max
    a_c = Nx.subtract(tensor_a, c)
    exp_a = Nx.exp(a_c)
    Nx.divide(exp_a, Nx.sum(exp_a))
  end

3.5.3 ソフトマックス関数の特徴

  • ソフトマックス関数の出力の総和は1になるという性質がある。
    • この性質のおかげで、ソフトマックス関数の出力を「確率」として解釈できる。
  • ソフトマックス関数を利用する事で、統計的な対応が可能になる。
  • ソフトマックス関数を適用して各要素の大小関係は変わらない。
  • ソフトマックス関数は出力層実装を省略できるというか省略が一般的
    • 詳しくは書籍参照(P.70)
iex()> Activator.softmax([0.3, 2.9, 4.0])
#Nx.Tensor<
  f32[3]
  [0.018211273476481438, 0.24519182741641998, 0.736596941947937]
>
iex()> y = Activator.softmax([0.3, 2.9, 4.0])
#Nx.Tensor<
  f32[3]
  [0.018211273476481438, 0.24519182741641998, 0.736596941947937]
>
iex()> Nx.sum(y)
#Nx.Tensor<
  f32
  1.0
>

3.5.4 出力層のニューロンの数

  • 出力層のニューロンの数は解くべき問題に応じて、適宜決める必要がある。
  • クラス分類を行う問題は、出力層のニューロンの数は分類したいクラスの数に設定するのが一般的
    • 例: 入力画像 0 ~ 9ののどれかを予測する問題は 出力層のニューロンを10個にする必要がある

3.6 手書き数字認識

学習済みのパラメータを使って推論処理だけをやってみよう。
この推論処理はニューラルネットワークの順方向伝播(foward propagation)

3.6.1 MNISTデータセット

  • ここで使用するデータはMNISTと言われる数字の画像データ
  • 訓練画像 60,000枚 テスト画像 10,000枚
  • 訓練画像を使って、学習を行い。学習データを使ってテストする
  • 28 x 28 のグレー画像(1チャンネル)
  • 各ピクセルは0 ~ 255

MNISTのデータを取得するためのライブラリを追加

deps do
    [
      {:nx, "~> 0.4.1"},
      {:expyplot, "~> 1.2"},
      {:scidata, "~> 0.1.9"} #<- 追加
    ]
end

MNISTデータの取得
取得方法はGitHubの README.md を参考に、とりあえず触ってみる事に。

を見ると以下のような例があるので確認しながら見ていきます。

{train_images, train_labels} = Scidata.MNIST.download() #学習用データ取得
{test_images, test_labels} = Scidata.MNIST.download_test() #テスト用データ取得

# Normalize and batch images
{images_binary, images_type, images_shape} = train_images

batched_images =
  images_binary
  |> Nx.from_binary(images_type)
  |> Nx.reshape(images_shape)
  |> Nx.divide(255)
  |> Nx.to_batched(32)
iex()> {train_images, train_labels} = Scidata.MNIST.download()
{{<<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, ...>>,
  {:u, 8}, {60000, 1, 28, 28}},
 {<<5, 0, 4, 1, 9, 2, 1, 3, 1, 4, 3, 5, 3, 6, 1, 7, 2, 8, 6, 9, 4, 0, 9, 1, 1,
    2, 4, 3, 2, 7, 3, 8, 6, 9, 0, 5, 6, 0, 7, 6, 1, 8, 7, 9, 3, 9, 8, ...>>,
  {:u, 8}, {60000}}}

iex()> train_images #{バイナリデータ、 tensorの型, tensorの形}
{<<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, ...>>,
 {:u, 8}, {60000, 1, 28, 28}}

iex()> train_labels #{ラベル、tensorの型、tensorの型} 
{<<5, 0, 4, 1, 9, 2, 1, 3, 1, 4, 3, 5, 3, 6, 1, 7, 2, 8, 6, 9, 4, 0, 9, 1, 1, 2,
   4, 3, 2, 7, 3, 8, 6, 9, 0, 5, 6, 0, 7, 6, 1, 8, 7, 9, 3, 9, 8, 5, 9, ...>>,
 {:u, 8}, {60000}}


iex()>  {images_binary, images_type, images_shape} = train_images
{<<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, ...>>,
 {:u, 8}, {60000, 1, 28, 28}}
iex()> images_type
{:u, 8}
iex(4)> images_shape
{60000, 1, 28, 28}

iex()> Nx.from_binary(images_binary, images_type)
#Nx.Tensor<
  u8[47040000]
  [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()> |> Nx.reshape(images_shape) 
#Nx.Tensor<
  u8[60000][1][28][28]
  [
    [
      [
        [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()>  |> Nx.divide(255)
#Nx.Tensor<
  f32[60000][1][28][28]
  [
    [
      [
        [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, 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()> 
#Stream<[enum: 0..1874, funs: [#Function<47.108234003/1 in Stream.map/2>]]>

なるほど、こういう風にデータ整形してたのね。なんとなくデータの形がわかったので、P75の画像を表示するのを試してみます。

images =
  images_binary
  |> Nx.from_binary(images_type)
  |> Nx.reshape(images_shape)
  |> Nx.divide(255)

img = images[0]
  |> Nx.reshape({28,28})

ここまで実装して、画像出力方法を調べてる時に @piacerex さんが記事にしていたこちらを参考にさせていただきました。

iex()> Nx.to_heatmap img                                

ただし、これ構造体の中に表示された状態で、ちょっと画像化とは違うので、画像化についてはまた後で別にチャレンジしたいと思います。

スクリーンショット 2023-01-14 18.16.32.png

3.6.2 ニューラルネットワークの推論処理

推論処理を行うニューラルネットワークを実装しよう。と書かれています。詳しい説明はテキストに任せます。(p.75)
簡単に言うと

  • 入力層 784 -> 画像サイズ28 x 28 = 784
  • 出力層 10 -> 0 ~ 9 の 10クラス
  • 隠れ層 1 -> 50個のニューロン
  • 隠れ層  2 -> 100のニューロン
    • 隠れ層の50、100は任意で決めている

上記を実装します。

get_data(), init_network(), predict() の実装

実装する際にPKLファイルを読み込むような記載があり、とりあえず中身を見てみようと読み込んでみる。
感で、多分 Floatの値が入ってるのでは?と <<x::float, _::bitstring>> で試してみると当たってそうだったので、とりあえず3つほど値を覗けるように x,y,z にバインドしてみました。

iex()> {:ok, binary} = File.read "./pkl/sample_weight.pkl"
{:ok,
 <<128, 3, 125, 113, 0, 40, 88, 2, 0, 0, 0, 98, 50, 113, 1, 99, 110, 117, 109,
   112, 121, 46, 99, 111, 114, 101, 46, 109, 117, 108, 116, 105, 97, 114, 114,
   97, 121, 10, 95, 114, 101, 99, 111, 110, 115, 116, 114, 117, ...>>}
iex()> <<x::float,y::float,z::float,  _::bitstring>> = binary
<<128, 3, 125, 113, 0, 40, 88, 2, 0, 0, 0, 98, 50, 113, 1, 99, 110, 117, 109,
  112, 121, 46, 99, 111, 114, 101, 46, 109, 117, 108, 116, 105, 97, 114, 114,
  97, 121, 10, 95, 114, 101, 99, 111, 110, 115, 116, 114, 117, 99, 116, ...>>
iex()> x
-4.85345000611663e-309
iex() y
2.083736988296e-312
iex() z
1.239276583315587e224

なんとなく中身はわかったのですが、多分 Pythonのデータ形式で Python側でElixirで扱える形式にしないといけないのかな。と調べ始めに、 @the_haigo さんのNxで始めるゼロから作るディープラーニング 準備編のことを思い出し、参考に確認しに行くと、ありがたい事に、知らない事がわんさか書かれていたので、調べる。

まず、pklファイルの中のデータ形式がndarray形式という事でndarrayを学習。以下参照

pythonを呼び出してファイルを読み込み elixirのデータに変換して使用します
ライブラリにはErlPortを使用します

で、準備編の記事に以下書かれて、そんな便利なライブラリがあったのね。と思いつつも利用方法を学習。

実装については、ここは @the_haigo さんの記事を参考にしたのでこちらもご覧ください。

さて、ErlPortを読み込んで利用できる環境を作っていきます。環境変数に追加していきます。

 defp deps do
    [
       #省略
      {:erlport, "~> 0.10.1"}
    ]
  end

続いて、まずは簡単なpythonコードを書いて、無事に実行できるか確認していきます。

@taashi さんが書いていたこちらの[ Pythonでコマンドライン引数を受け取る ]記事を参考にさせていただきました。ありがとうございます。

pkl/hello.py
import sys

def call(st):
    print(st, 'Hello Python')
    return (st + 'Hello Python')

if __name__ == '__main__':
    args = sys.argv
    if 2 <= len(args):
        if args[1]:
            call(args[1])
        else:
            print('Argument is not')
    else:
        print('Arguments are too short')

一旦、Pythonで実行して無事に動くか確認してみます。

$ python hello.py YOSUKE
YOSUKE Hello Python

無事に動いたので、
これをElixirから実行してみたいと思います。

iex()> {:ok, pid} = :python.start([python_path: './pkl']) 
{:ok, #PID<0.1050.0>}
iex()> result = :python.call(pid, :hello, :call, ["YOSUKE!!"])
('YOSUKE!!', 'Hello Python')
"YOSUKE!!Hello Python"
iex()> :python.stop pid
:ok

{:ok, pid} = :python.start([python_path: './pkl'])
ここでは、呼び出したいpythonのファイルがある場所までのパスを入力してます。

:python.callの引数ですが、第一引数は PID , 第2引数はhello.pyを呼び出すファイル名を:helloアトムにして呼び出してます。第3引数で呼び出す関数を:callとアトム形式で呼び出し、第4引数では、[”引数1"]call関数に必要な引数を引渡してます。

で、実行してみるとunsupported pickle protocol: 3の例外エラーでハマる。
半日調べて、@kabayan55 さんが書かれていた、これか?というものに気が付く。

と言うことで早速変換してみる。

pkl/dump.py
import pickle

with open("sample_weight.pkl", "rb") as f:
    w = pickle.load(f)

pickle.dump(w, open("sample_weight2.pkl","wb"), protocol=2)

今度は、引数エラー、、、という事で、このエラー解決ができず1日ハマってしまいました。

で、とりあえず。一旦諦めて、中身を調べてElixir側でハードに扱うことに。と言う事で、pklを読み込んでprintしてみたら以下のような形式だったので、json形式にエンコードする事にしました。

{'b2': array([-0.01471108, -0.07215131, -0.00155692,  0.12199665,  0.11603302,
       -0.00754946,  0.04085451, -0.08496164,  0.02898045,  0.0199724 ,
#省略
       [ 1.07227898e+00, -3.73002291e-01, -3.47672790e-01,
        -6.34944439e-01, -2.26390660e-01,  8.66467118e-01,
        -1.20632663e-01,  1.60931930e-01, -2.61490524e-01,
         3.17717306e-02]], dtype=float32), 'b3': array([-0.06023985,  0.00932628, -0.01359946,  0.02167128,  0.0107372 ,
        0.06619699, -0.08397342, -0.00912251,  0.00576962,  0.0532335 ],
      dtype=float32)}

という事で、pythonでnumpyのndarrayをjsonにエンコードするコードを書いて実行。

import pickle
import numpy
import json
from json import JSONEncoder

class NumpyArrayEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, numpy.ndarray):
            return obj.tolist()
        return JSONEncoder.default(self, obj)

with open("sample_weight.pkl", "rb") as f:
    pkl = pickle.load(f, encoding='utf-8')

with open("pklData.json", "w") as write_file:
    json.dump(pkl, write_file, cls=NumpyArrayEncoder)

無事に完了したので、今度はこれをElixir側で読み込んでNx.tensor形式に変換する処理を書いていきます。
なんだか遠回りしてるなぁ。。。

ちなみに、長くなりすぎたので、後編をさらに分割する事にします。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5