そんな今日この頃の技術ネタ

本家側に書くほどでもない小ネタ用

Deep Learning学習その1 ひとまずChainerを動かしてみる

 セミナーで学んだ内容を自分なりに噛み砕いてまとめて復習してみる。

  • ニューラルネットワークとは、さながら脳のシナプス結合のようにして個別に数値処理をするノードを積み重ねてた処理のモデルである -> 世の中の事象を何らかの数列操作として表現する -> 入出力は数列で表現できる必要がある
  • 個別のノードは複数の入力xから望ましいuが求まる数式f(x) = w_1 * x_11 + w_2 * x_11 + ・・・として示すことができる -> 一番最初はランダムに重みwが振られ、それぞれの入力に対する適切な値を重回帰分析により求めて調整していくことが学習である
  • 中間の層と層の間でRelu関数などの非線形変換を噛ませることで世の中の多様な事象の表現を実現できる
  • 学習は現状の重みを基にして望ましい数値との誤差を計算する順伝播と、その誤差から個々のノードを調整する逆伝播の繰り返しである


 実際の流れとして

  1. 訓練データを数列として与える
  2. 何層かとか入出力の数列の長さとかノード間の結合といったモデルを設計して定義する -> ここシステマティックにバシッと決まるわけではないのがプログラマ的には気持ち悪い
  3. 誤差からの調整方法なんかを定義する -> ここも目的によっていくつか方法がある
  4. 2と3を用いて学習を繰り返す -> ここの回数なんかも動かして誤差の収束状況なんかと見比べて調整する感じ
  5. 学習済みモデルの出来上がり、上手く出来上がれば入力を突っ込むとそれらしい出力が出るようになる


 これを僕の中ではスマートボール(跳ね返し板の無いピンボール)として連想している。

f:id:blue1st:20170919003146p:plain

  • 板のサイズだの球数だのは決まっている->モデルは人間が予め設計する必要がある
  • 特定の位置に置いた球が落ちるべき場所に転がるように釘を調整するのが学習


 そんなこんなでひとまずChainer 2.1.0にて実際にコード。実際は数値を調整したりなのでJupyter notebook上にて実行。(本当はGitHub上のipynbファイルにリンクすれば良かったんだけど、半端に外に出しちゃいけないファイルと同じディレクトリにしちゃったのは失敗だったな・・・)

 -3〜3までの範囲のsin関数を対象としてみた。

0. 準備

 ログ見たり収束状況確認したりする用。

# 基本的なモジュール
import numpy as np
import pandas  as pd
import matplotlib.pyplot as plt
import json

 ひとまず愚直に。この辺は慣れればもっとスマートな書き方にできると思う。

# Chainer関連のモジュール
import chainer
import chainer.links as L
import chainer.functions as F
from chainer import Chain
from chainer import Variable
from chainer import report
from chainer import optimizers
from chainer import training
from chainer.training import extensions
from chainer.datasets import split_dataset_random

1. データセット・・・球の投入位置と落ちるべき場所の関係性

 xとして-3〜3までを100分割した数列、あるべき教師データtとしてそのsin関数の出力を代入。

# 標本データの作成
x = np.linspace(-3, 3, 100).reshape(100, 1)
t = np.sin(x)
plt.scatter(x, t)
plt.show()

 ちゃんと正弦波。

f:id:blue1st:20170919004545p:plain

 chainerで用意されているsplit_dataset_randomメソッドを用いて訓練データを検証データをランダムに振り分ける。

# データセットを作成して訓練データと検証データに分割
x = np.array(x, dtype = np.float32)
t = np.array(t, dtype = np.float32)

dataset = list(zip(x, t))

n_train = int(len(dataset) * 0.7)
train, test = split_dataset_random(dataset, n_train)

2. モデルの宣言、盤面の設計

 以下のようなモデルを定義。

# モデルの作成
class NN(Chain):
    # 中間層3層を定義
    def __init__(self, n_units1, n_units2, n_output):
        super().__init__()
        with self.init_scope():
            self.l1= L.Linear(None, n_units1)
            self.l2 = L.Linear(None, n_units2)
            self.l3 = L.Linear(None, n_output)
    
    # 学習用のメソッド
    def __call__(self, x, t, train = True):
        # 与えられたxから推論を行い、その結果と教師データの誤差を返す
        y = self.predict(x)
        loss = F.mean_squared_error(y, t)
        if train:
            report({'loss': loss}, self)
        return loss
    
    # 推論
    def predict(self, x):
        z1 = F.relu(self.l1(x))
        z2 = F.relu(self.l2(z1))
        return self.l3(z2)

 具体的なノード数を与えてインスタンス化。

# モデルの宣言 100層->30層->1出力
model = NN(100, 30, 1)

3. モデルの調整方法とか学習方法とかの定義・・・釘の打ち方の方針

 optimizerとしてはSGDとか色々あるけど、ひとまず最初はAdamを選んでおくのが無難らしい。

# optimizerの宣言
optimizer = optimizers.Adam()
optimizer.setup(model)
# updaterの宣言
batchsize = 20
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat = False, shuffle = False)
updater = training.StandardUpdater(train_iter, optimizer)

4. 学習・・・試打しては調整を繰り返す

 泥臭く書けもするけど今時はtrainerを使うのが良いらしい。ログとして誤差なんかを出力する。

# 学習
epoch = 1000
trainer = training.Trainer(updater, (epoch, 'epoch'), out = 'result')
trainer.extend(extensions.Evaluator(test_iter, model))
trainer.extend(extensions.LogReport(trigger=(1, 'epoch')))
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'validation/main/loss', 'elapsed_time']), trigger=(1, 'epoch'))
trainer.run()

 以下のようなログが出力される。

epoch       main/loss   validation/main/loss  elapsed_time
1           1.05255     0.656665              0.0104949     
2           0.56132     0.361226              0.0231535     
3           0.327868    0.212045              0.0420246     
4           0.22586     0.171785              0.0570989     
5           0.193939    0.166911              0.0761545     
・・・
996         0.00015972  0.000244043           27.8698       
997         0.000186852  7.72238e-05           27.9297       
998         0.000192414  0.000421348           27.9939       
999         0.000270272  7.86659e-05           28.0605       
1000        0.000132944  0.000277              28.1255     

 パッと見でも訓練データの誤差を示すmain/lossと検証データの誤差を示すvalidation/main/lossの値が共に下がっていっていることが分かる。

 ちゃんとグラフ化すると・・・

# 学習結果の表示
with open('result/log') as f:
    logs = json.load(f)

result_train = [log['main/loss'] for log in logs]
result_test = [log['validation/main/loss'] for log in logs]

plt.plot(result_train, label = 'train')
plt.plot(result_test, label = 'test')
plt.legend()
plt.show()

f:id:blue1st:20170919010416p:plain

良い感じに収束していっていること、これ以上学習を繰り返しても意味がなさそうなことが確認できる。

5. モデル完成・・・実際に打ち込む

 そんなこんなで、訓練データたるt=sin(x)と今回作成したモデルにxを与えた時の出力yを比べると・・・

# モデルを使用した予測値
y = model.predict(x).data
plt.plot(x, t, label = 't')
plt.scatter(x, y, label = 'y')
plt.legend()
plt.show()

f:id:blue1st:20170919010713p:plain

良い感じにsin関数を再現できていることが確認できる。

 じゃあってんでもっと先の数値を与えると・・・

# 範囲の違う値を予測してみる
x_ = np.linspace(1, 6, 100).reshape(100, 1)
x_ = np.array(x_, dtype=np.float32)
plt.scatter(x_, model.predict(x_).data)
plt.show()

f:id:blue1st:20170919010927p:plain

と、このように学習で与えた範囲以降は点でsin関数とは違う値を示してしまう。今回のコードではあくまで-3〜3の範囲で擬似的にsin関数の出力を再現するモデルが作成されただけであり、sin関数そのものではないのである。

 このへんは時系列(=前の値)を考慮できるLSTMを利用すると上手いこと再現できるに違いない。次はそのあたりに挑戦してみたいと思う。

Chainerで学ぶディープラーニング入門

Chainerで学ぶディープラーニング入門