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

はじめに

言葉の定義から

【Define and Run】

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

【Define by Run】

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

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

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

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

import

In [2]:
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
C:\Anaconda3\envs\chainer\lib\site-packages\h5py\__init__.py:36: FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`.
  from ._conv import register_converters as _register_converters

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

オリジナルのネットワーク『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 [27]:
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 [28]:
# NNモデルを宣言
model = MyChain()
print(model)
<__main__.MyChain object at 0x0000022AB4DD8438>

実験データ生成

In [30]:
# 実験データ用の配列
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[30]:
X Y
0 [0] [0.5154791095907731]
1 [1] [0.996363169120714]
2 [2] [0.7230978254277565]
3 [3] [0.43119232108533867]
4 [4] [0.26658103872815797]
5 [5] [0.713699922246066]
6 [6] [0.02804603772317027]
7 [7] [0.27734516243523943]
8 [8] [0.6783651850432646]
9 [9] [0.009792034130903349]
In [31]:
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[31]:
array([[0.5154791 ],
       [0.99636316],
       [0.7230978 ],
       [0.4311923 ],
       [0.26658103],
       [0.71369994],
       [0.02804604],
       [0.27734515],
       [0.6783652 ],
       [0.00979203]], dtype=float32)

学習

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

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

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

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

In [32]:
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 [37]:
# 学習過程グラフを一部拡大
plt.plot(step, loss_list)
plt.xlim([10000, 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 [38]:
# 教師データ(実験データ)
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.34604314]])

推論結果❷

活性化関数をreluに固定

In [39]:
# 教師データ(実験データ)
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.45657268]])

推論結果❸

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

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

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

この知見を踏まえ…

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

In [40]:
# 教師データ(実験データ)
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([[4.3491426]]) ⇒ leaky_reluを選択
xi = variable([[4.7033844]]) ⇒ leaky_reluを選択
xi = variable([[7.969956]]) ⇒ reluを選択
xi = variable([[5.1330886]]) ⇒ reluを選択
xi = variable([[3.1755862]]) ⇒ leaky_reluを選択
xi = variable([[8.821309]]) ⇒ reluを選択
xi = variable([[2.8019352]]) ⇒ leaky_reluを選択
xi = variable([[1.2076008]]) ⇒ leaky_reluを選択
xi = variable([[1.0058092]]) ⇒ leaky_reluを選択
xi = variable([[4.1335163]]) ⇒ leaky_reluを選択
variable([[0.45657268]])

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

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

まとめ

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

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

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

【Define by Run ポイント】

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

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

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

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

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

本記事を参考に…

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

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

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

(完)