【Chainer】Define by Runで動的ニューラルネットワーク設計

はじめに

言葉の定義から

【Define and Run】

  • 静的なニューラルネットワーク
  • ニューラルネットワークを固定(定義)⇒そのニューラルネットワークにデータを流して演算

【Define by Run】

  • 動的なニューラルネットワーク
  • データを流しながらニューラルネットワークの固定(定義)と演算を行う

前回書いた以下の記事は【Define and Run】つまり静的ニューラルネットワークを設計しました

【深層学習入門】超実践!Chainerと深層学習でシステム解析する方法

今回は↑の記事と同じ課題で【Define by Run】つまり動的ニューラルネットワークを設計します!

Chainerインストール

In [1]:
!apt -y install libcusparse8.0 libnvrtc8.0 libnvtoolsext1
!ln -snf /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so.8.0 /usr/lib/x86_64-linux-gnu/libnvrtc-builtins.so
!pip install 'cupy-cuda80' 'chainer'
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following NEW packages will be installed:
  libcusparse8.0 libnvrtc8.0 libnvtoolsext1
0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
Need to get 28.9 MB of archives.
After this operation, 71.6 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libcusparse8.0 amd64 8.0.61-1 [22.6 MB]
Get:2 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libnvrtc8.0 amd64 8.0.61-1 [6,225 kB]
Get:3 http://archive.ubuntu.com/ubuntu artful/multiverse amd64 libnvtoolsext1 amd64 8.0.61-1 [32.2 kB]
Fetched 28.9 MB in 0s (57.0 MB/s)
Selecting previously unselected package libcusparse8.0:amd64.
(Reading database ... 18408 files and directories currently installed.)
Preparing to unpack .../libcusparse8.0_8.0.61-1_amd64.deb ...
Unpacking libcusparse8.0:amd64 (8.0.61-1) ...
Selecting previously unselected package libnvrtc8.0:amd64.
Preparing to unpack .../libnvrtc8.0_8.0.61-1_amd64.deb ...
Unpacking libnvrtc8.0:amd64 (8.0.61-1) ...
Selecting previously unselected package libnvtoolsext1:amd64.
Preparing to unpack .../libnvtoolsext1_8.0.61-1_amd64.deb ...
Unpacking libnvtoolsext1:amd64 (8.0.61-1) ...
Setting up libnvtoolsext1:amd64 (8.0.61-1) ...
Setting up libcusparse8.0:amd64 (8.0.61-1) ...
Setting up libnvrtc8.0:amd64 (8.0.61-1) ...
Processing triggers for libc-bin (2.26-0ubuntu2.1) ...
Collecting cupy-cuda80
  Downloading https://files.pythonhosted.org/packages/de/e2/a335e07f9b40b7e16a1aa04924b18c538ce763237ac7b5137caffc1c9a3a/cupy_cuda80-4.5.0-cp36-cp36m-manylinux1_x86_64.whl (200.7MB)
    100% |████████████████████████████████| 200.7MB 205kB/s 
Collecting chainer
  Downloading https://files.pythonhosted.org/packages/fc/ae/5b08a1717187ef98127b5d68ec700009b9c8a918b40319813e3e89f2254f/chainer-4.5.0.tar.gz (402kB)
    100% |████████████████████████████████| 409kB 17.1MB/s 
Collecting fastrlock>=0.3 (from cupy-cuda80)
  Downloading https://files.pythonhosted.org/packages/b5/93/a7efbd39eac46c137500b37570c31dedc2d31a8ff4949fcb90bda5bc5f16/fastrlock-0.4-cp36-cp36m-manylinux1_x86_64.whl
Requirement already satisfied: six>=1.9.0 in /usr/local/lib/python3.6/dist-packages (from cupy-cuda80) (1.11.0)
Requirement already satisfied: numpy>=1.9.0 in /usr/local/lib/python3.6/dist-packages (from cupy-cuda80) (1.14.6)
Collecting filelock (from chainer)
  Downloading https://files.pythonhosted.org/packages/85/1c/389ca4da8b631a06dec64c94c9c6f22bbd9be236f0030ee4863e7d6e42a7/filelock-3.0.8-py3-none-any.whl
Requirement already satisfied: protobuf>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from chainer) (3.6.1)
Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf>=3.0.0->chainer) (39.1.0)
Building wheels for collected packages: chainer
  Running setup.py bdist_wheel for chainer ... - \ | / - \ done
  Stored in directory: /root/.cache/pip/wheels/4f/a5/e6/b0cbcc4cc1daeaab566096480d5b9aed9106de601daf1006f8
Successfully built chainer
Installing collected packages: fastrlock, cupy-cuda80, filelock, chainer
Successfully installed chainer-4.5.0 cupy-cuda80-4.5.0 fastrlock-0.4 filelock-3.0.8

import

In [0]:
import math
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from chainer import Chain, Variable
import chainer.functions as F
import chainer.links as L
from chainer import optimizers

動的ニューラルネットワーク構築

オリジナルのネットワーク『MyChain』を作る!

Chainerで使える活性化関数

Chainerで使える活性化関数のうち、前回も採用した”relu”と新たに”leaky_relu”を使ってみる。

”relu””leaky_relu”を可視化する↓

In [3]:
x = np.arange(-10, 10, 0.1)
x = Variable(np.array(x, dtype=np.float32))
y_r = F.relu(x)
y_lr = F.leaky_relu(x)

# グラフ出力
plt.plot(x.data, y_r.data)
plt.title("relu")
plt.xlabel("x axis")
plt.ylabel("y axis")
plt.grid(True)
plt.show()

plt.plot(x.data, y_lr.data)
plt.title("leaky_relu")
plt.xlabel("x axis")
plt.ylabel("y axis")
plt.grid(True)
plt.show()

設計コンセプト

今回は、活性化関数のみ動的にします。

”leaky_relu”採用理由

実は活性化関数って、まだ謎が多くて「”relu”を使うとなぜ上手くいくのか?」という数学的な根拠があまりないらしい…

流れてきたデータが負数の場合はノイズになるので”relu”を使って除去すると良い…というのが定説

でも、負数を完全に除去するって…なんか勿体なくないですか?
うまく言えないけど、マイナスとはいえ完全になかったことにするなんて…

勉強で苦しいとき!マイナスからのスタート!そこから得られる情報だって必ずあると思うんだよなぁ

ということで、”leaky_relu”を使て、負数の情報も少しだけ順伝播させます(前のノードに流す)。

活性化関数のみ動的に変更

突然ですが…子供の成長スピードは早い!
最初は私を怖がっていたのに、安全な人だと学習すると、笑顔を見せてくれます。

子供なら、成長と同時に脳内のニューロンが増えたり・減ったり・変化したりすると思いますが…
大人になると身体的な成長が小さい(ほぼ無い?)ので、脳内のニューロン構造ってある程度、固定されてると思います。(←なんとなくね)

でも、お風呂でリラックスしているときにアイデアを閃くことがある!
これって、『いつもと違う活性化関数が働いたのかなぁ』とか考えてしまう!笑

  • 大人の脳⇒ニューロン構造固定⇒ニューラルネットワーク固定
  • 大人の脳でも閃く⇒閃く前後でニューロン構造が変化したとは考えにくい?⇒活性化関数が違うのかも!

みたいなことを、お風呂でリラックスしているときに閃いたので、実践する!!

動的ニューラルネットワーク設計

  • スイッチのON/OFFのように活性化関数を切り替えられるように設計
  • スイッチのON/OFFを柔軟に切り替えられるように、引数でスイッチのパラメータを受け取る
In [0]:
class MyChain(Chain):

    def __init__(self):
        super(MyChain, self).__init__(
            l1 = L.Linear(1, 100),
            l2 = L.Linear(100, 50),
            l3 = L.Linear(50, 1)
        )

    def predict(self, x, sw_func):
        if sw_func == 1:
            h1 = F.leaky_relu(self.l1(x))
            h2 = F.leaky_relu(self.l2(h1))
        else:
            h1 = F.relu(self.l1(x))
            h2 = F.relu(self.l2(h1))
        return self.l3(h2)

NNモデル宣言

In [14]:
# NNモデルを宣言
model = MyChain()
print(model)
<__main__.MyChain object at 0x7feb598215c0>

実験データ生成

In [17]:
# 実験データ用の配列
x = [] 
y = []
get_values = 0

for i in range(10):
    get_values = random.random()
    x.append([i])
    y.append([get_values])

# データフレーム生成(列基準)
df = pd.DataFrame({'X': x, 
                   'Y': y})

# グラフ出力
plt.plot(x, y)
plt.title("Training Data")
plt.xlabel("input_x axis")
plt.ylabel("output_y axis")
plt.grid(True)

df
Out[17]:
X Y
0 [0] [0.0775113651538859]
1 [1] [0.047005641501278284]
2 [2] [0.2160034604416825]
3 [3] [0.13448020038429298]
4 [4] [0.803185484248121]
5 [5] [0.5088926054221293]
6 [6] [0.572910739239348]
7 [7] [0.20810551362151908]
8 [8] [0.38727295145995677]
9 [9] [0.9883437714232438]
In [18]:
x = Variable(np.array(x, dtype=np.float32))
y = Variable(np.array(y, dtype=np.float32))

# x.data
y.data

# print(x)
# print(y)
Out[18]:
array([[0.07751136],
       [0.04700564],
       [0.21600346],
       [0.1344802 ],
       [0.80318546],
       [0.5088926 ],
       [0.5729107 ],
       [0.20810552],
       [0.38727295],
       [0.9883438 ]], dtype=float32)

学習

前回と同じ 8万回 学習させました!

0~1の範囲で乱数を生成し、

  • 0.8より大きな値:ON(leaky_relu)
  • それ以外の値:OFF(relu)

なんとなく、閃きを”leaky_relu”ということにし、たまにしか発生しないように設定しました。

In [19]:
rand_val = 0
sw_func = 0
log_sw = []

# 損失関数の計算(二乗誤差(MSE)を採用)
def forward(x, y, model):
    # 活性化関数の切替
    rand_val = np.random.random()
    if rand_val > 0.8:
        sw_func = 1
    else:
        sw_func = 0
    
    log_sw.append([sw_func])
    
    t = model.predict(x, sw_func)
    loss = F.mean_squared_error(t, y)
    return loss

# 最適化アルゴリズムにAdamを採用
optimizer = optimizers.Adam()
optimizer.setup(model)

# パラメータの学習を繰り返す
loss_list = []
step = []
for i in range(0, 80000): # 10000
    loss = forward(x, y, model)
    step.append(i)
    loss_list.append(loss.data)
    # print("loss: {}".format(loss.data))
    optimizer.update(forward, x, y, model)

# 学習過程
plt.plot(step, loss_list)
plt.title("Training Data")
plt.xlabel("step")
plt.ylabel("loss")
plt.grid(True)
plt.show()

考察

Lossが下がるところと活性化関数の切替を確認

In [21]:
# 学習過程グラフを一部拡大
plt.plot(step, loss_list)
plt.xlim([7000, 20000])
plt.ylim([0,0.1])
plt.title("Training Data")
plt.xlabel("step")
plt.ylabel("loss")
plt.grid(True)
plt.show()

# 活性化関数の切替
plt.plot(log_sw)
plt.xlim([0,50])
plt.ylim([0,1.5])
plt.title("Activation Function")
plt.xlabel("step")
plt.ylabel("Random")
plt.grid(True)
plt.show()

すごいノイズ乗ってるけど、1万回くらいでlossが急降下しています。
前回は、見られなかった現象…偶然の可能性もあるけど興味深いね!

活性化関数の切替も良い感じにランダムだ(まるで私の脳内のようだ!?)

推論

学習により自動調整した”重み”は固定されますが、推論でも活性化関数は切り替えることができます。

活性化関数を

  1. leaky_reluに固定
  2. reluに固定
  3. leaky_relureluを切り替える

の3パターンで推論してみます。

推論結果❶

活性化関数をleaky_reluに固定

In [22]:
# 教師データ(実験データ)
plt.plot(x.data, y.data)
plt.title("Training Data")
plt.xlabel("x axis")
plt.ylabel("y axis")
plt.grid(True)
plt.show()

# 推論結果
sw = 1
ym = model.predict(x, sw)
plt.plot(x.data, ym.data)
plt.title("Predict")
plt.xlabel("input x")
plt.ylabel("output ym")
plt.grid(True)
plt.show()

# 推論結果2
xt = [[0.5], [1.8], [2.3], [3.3], [4.5], [6.8], [7.2], [7.7], [8.0], [8.2]]
xt = Variable(np.array(xt, dtype=np.float32))
yt = model.predict(xt, sw)
plt.plot(xt.data, yt.data, "ro")
plt.title("Predict2")
plt.xlabel("input xt")
plt.ylabel("output yt")
plt.grid(True)
plt.show()

# グラフを重ねる
plt.plot(x.data, y.data)
plt.plot(x.data, ym.data)
plt.plot(xt.data, yt.data, "ro")
plt.title("comparison")
plt.xlabel("input")
plt.ylabel("output")
plt.grid(True)
plt.show()

n = [[5.5]]
n = Variable(np.array(n, dtype=np.float32))
yn = model.predict(n, sw)
print(yn)
variable([[0.5465759]])

推論結果❷

活性化関数をreluに固定

In [23]:
# 教師データ(実験データ)
plt.plot(x.data, y.data)
plt.title("Training Data")
plt.xlabel("x axis")
plt.ylabel("y axis")
plt.grid(True)
plt.show()

# 推論結果
sw = 0
ym = model.predict(x, sw)
plt.plot(x.data, ym.data)
plt.title("Predict")
plt.xlabel("input x")
plt.ylabel("output ym")
plt.grid(True)
plt.show()

# 推論結果2
xt = [[0.5], [1.8], [2.3], [3.3], [4.5], [6.8], [7.2], [7.7], [8.0], [8.2]]
xt = Variable(np.array(xt, dtype=np.float32))
yt = model.predict(xt, sw)
plt.plot(xt.data, yt.data, "ro")
plt.title("Predict2")
plt.xlabel("input xt")
plt.ylabel("output yt")
plt.grid(True)
plt.show()

# グラフを重ねる
plt.plot(x.data, y.data)
plt.plot(x.data, ym.data)
plt.plot(xt.data, yt.data, "ro")
plt.title("comparison")
plt.xlabel("input")
plt.ylabel("output")
plt.grid(True)
plt.show()

n = [[5.5]]
n = Variable(np.array(n, dtype=np.float32))
yn = model.predict(n, sw)
print(yn)
variable([[0.5413668]])

推論結果❸

推論結果❶❷を観測すると…

  • 入力値x > 5 のときreluの方が精度が良い
  • 入力値x <= 5 のときleaky_reluの方が精度が良い

という結果が得られた。(僅差だけどね!)

この知見を踏まえ…

入力データに応じて、活性化関数をreluとleaky_reluを切り替える

In [24]:
# 教師データ(実験データ)
plt.plot(x.data, y.data)
plt.title("Training Data")
plt.xlabel("x axis")
plt.ylabel("y axis")
plt.grid(True)
plt.show()

# 推論結果
ym = model.predict(x, sw)
plt.plot(x.data, ym.data)
plt.title("Predict")
plt.xlabel("input x")
plt.ylabel("output ym")
plt.grid(True)
plt.show()

'''
# 推論結果2
xt = [[0.5], [1.8], [2.3], [3.3], [4.5], [6.8], [7.2], [7.7], [8.0], [8.2]]
xt = Variable(np.array(xt, dtype=np.float32))
yt = model.predict(xt, sw)
plt.plot(xt.data, yt.data, "ro")
plt.title("Predict2")
plt.xlabel("input xt")
plt.ylabel("output yt")
plt.grid(True)
plt.show()
'''

# 推論結果3 -入力値から活性化関数を変更-
# log_xi = []
# log_yi = []
sw_i = 0
for i in range(10):
    tmp = random.uniform(0.0, 9.0)
    xi = [[tmp]]
    xi = Variable(np.array(xi, dtype=np.float32))
    if xi.data > 5:
        sw_i = 1
        print('xi = {} ⇒ reluを選択'.format(xi))
    else:
        sw_i = 0
        print('xi = {} ⇒ leaky_reluを選択'.format(xi))
    yi = model.predict(xi, sw_i)
    plt.plot(xi.data, yi.data, "bo")

# plt.title("Predict3")
# plt.xlabel("input xi")
# plt.ylabel("output yi")
# plt.grid(True)
# plt.show()

# グラフを重ねる
plt.plot(x.data, y.data)
plt.plot(x.data, ym.data)
# plt.plot(xt.data, yt.data, "ro")
# plt.plot(xi.data, yi.data, "go")
plt.title("comparison")
plt.xlabel("input")
plt.ylabel("output")
plt.grid(True)
plt.show()

n = [[5.5]]
n = Variable(np.array(n, dtype=np.float32))
yn = model.predict(n, sw)
print(yn)
xi = variable([[7.1152024]]) ⇒ reluを選択
xi = variable([[8.4421625]]) ⇒ reluを選択
xi = variable([[2.5160198]]) ⇒ leaky_reluを選択
xi = variable([[8.506267]]) ⇒ reluを選択
xi = variable([[1.6752733]]) ⇒ leaky_reluを選択
xi = variable([[6.3592505]]) ⇒ reluを選択
xi = variable([[2.0420346]]) ⇒ leaky_reluを選択
xi = variable([[6.6776514]]) ⇒ reluを選択
xi = variable([[1.1869509]]) ⇒ leaky_reluを選択
xi = variable([[7.075601]]) ⇒ reluを選択
variable([[0.5413668]])

ズルできないように、最後の入力値は乱数で与えてみたけど…

ちょっち怖いくらい精度良いですね!!笑

まとめ

人生で初めて動的ニューラルネットワーク設計に挑戦してみましたが、深層学習の楽しさを再確認しました!!

今回は、活性化関数のみ動的にしましたが、ネットワーク構造(ノード数や層の深さ)も動的にできます。

学習時に活性化関数をランダムで切り替えましたが、ランダム以外の条件で切り替えても面白いかと

【Define by Run ポイント】

  • 活性化関数を切り替えられる
  • ネットワーク構造を切り替えられる
  • 切り替の条件も設定できる
  • 切り替えの設定はモデル(MyChain)class内外どちらも可(引数で切り替え条件を渡せる)
  • 学習だけでなく、推論でも Define by Run

組み合わせは星の数ほどありそうですね!!

私 1人では、すべての組み合わせを試すのは不可能です…

でも、本記事を読んくれた人は、動的ニューラルネットワークの設計方法を習得できたと思う!!

私に代わって、自分オリジナルのモデル(MyChain)を作ってみてほしい!!

本記事を参考に…

  • 今回と同じ課題で、1万回以下の学習で良い推論結果が出た!
  • 別の課題に対し、Define by Runやってみた!

なんて人が現れたディープラーニングお兄さんは最高に嬉しいです!!!!!

奥が深い深ーーい!深層学習を思いっきり楽しんで下さいな!

(完)