この投稿は 限定共有に設定されています。 投稿者本人及びURLを知っているユーザーのみアクセスすることができます。

機械学習のためのプログラミング講座 4時限目

  • 0
    コメント

はじめに

Deep Learningは既にオープンソースのライブラリが豊富に出まわっており、今回はその中でも国産で定評があり、とある記事でGPU計算が現状で割と速いと書かれていたchainerを使用します。

ただ、Chainerの入門記事は多くあるのですが、そのほとんどが手書き文字を認識するmnistのサンプルを実行して終わっています。
たしかに、mnistのサンプルを眺めれば、Chainerの使い方はわかってくるのですが、なんとなくわかるのと自分で組めるのは違うなということで、今回はchainerを使ったDeep Learningが自分で組めるというところまでを目標にやっていきます。

開発環境

・OS: Mac OS X EI Capitan (10.11.5)
・Python 2.7.12: Anaconda 4.1.1 (x86_64)
・chainer 1.12.0

chainerの環境を準備されていない方は、

$ pip install chainer

にて簡単にインストールを行うことができます。

順方向・逆方向の計算 (Forward/Backward Computation)

こちらでは、Neural Networkに踏み込む前段階である、変数を使った順方向と逆方向の計算についてです。

まず、chainerを読み込み、変数の宣言を行います。

>>> import chainer
>>> x_data = np.array([5], dtype=np.float32)
>>> x_data
array([ 5.], dtype=float32)

基本的には、numpyの配列のfloat型で宣言するようです。

chainer内で使用するための変数として、chainer.Variableを使用します。

>>> x = chainer.Variable(x_data)
>>> x
<variable at 0x10b796fd0>

xの値は .dataにて確認できます。

>>> x.data
array([ 5.], dtype=float32)

つぎに、x の関数 y を宣言します。
今回は以下のような関数を使用します。
$y = x^2 - 2x + 1$

>>> y = x ** 2 - 2 * x + 1
>>> y
<variable at 0x10b693dd0>

yの値も同様にして確認できます。

>>> y.data
array([ 16.], dtype=float32)

以下のメソッドを呼ぶことで、微分が計算できるようになるとのことです。

>>> y.backward()

誤差逆伝播法 (back-propagation) のための勾配は grad で求められます。

>>> x.grad
array([ 8.], dtype=float32)

どれに対する勾配か少しわかりにくいですが、yをxで微分した時の勾配の値

y'(x) = 2x - 2\\
\rightarrow \ y'(5) = 8

から x.grad8 という値が導き出されています。

Chainerの公式リファレンスには、xを多次元配列とする場合には、y.gradを初期化してから、x.gradを計算してねと書いてあります。
初期化をしないと勾配の値が格納されている配列に加算されていくらしく、「勾配計算の前には初期化」と覚えておきましょう。

>>> x = chainer.Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
>>> y = x**2 - 2*x + 1
>>> y.grad = np.ones((2, 3), dtype=np.float32)
>>> y.backward()
>>> x.grad
array([[  0.,   2.,   4.],
       [  6.,   8.,  10.]], dtype=float32)

Neural Networkモデルの構造を明示 (Links)

Neural Networkを構成するにあたり、どのような構造のモデルを構成するか(具体的には、ノードがいくつ、レイヤーがいくつ)を明示的に宣言します。
このモデルの決定はまだまだ経験と勘に頼るところですね。
Deep Learningを含めたNeural Networkは内部のハイパーパラメータこそ自動的に調整してくれるのですが、最初のモデルは事前に決定する必要があります。
ちょっと話はそれますが、ベイズ統計でもモンテカルロマルコフ連鎖(MCMC)ではベイズの定理に基づいて事後分布をうまく推定できるように設計されていますが、その場合でも事前分布を任意で決定する必要があります。
Neural Networkにしてもベイズ統計にしても、この辺りを解決する画期的な方法が提案されれば、どんな問題にも万能に対応できるよう予測モデル構築ができると期待しています。

話は戻りますが、ここで、pythonのコード内で、省略形でchainer呼び出せるようにしておきます。

>>> import chainer.links as L

Neural Networkを勉強している人ならわかると思うのですが、こちらのノード間には重みと呼ばれるパラメータを持っています。
とりあえず、一番単純な線形結合のパターンで試してみましょう。

>>> f = L.Linear(3, 2)
>>> f
<chainer.links.connection.linear.Linear object at 0x10b7b4290>

こちらは入力が3層で、出力が2層という構造を表しています。
Linear の部分について簡単に説明をしておくと、先ほど述べた線形結合でノード間を繋ぐということですので、以下の関係式で表されます。

f(x) = Wx + b\\
f \in \mathcal{R}^{2 \times 1},
x \in \mathcal{R}^{3 \times 1},\\
W \in \mathcal{R}^{2 \times 3}, b \in \mathcal{R}^{2 \times 1}

したがって、明示的に宣言はしていませんでしたが、上記で宣言した f はパラメータである重み行列 W と重みベクトル b を持っています。

>>> f.W.data
array([[-0.02878495,  0.75096768, -0.10530342],
       [-0.26099312,  0.44820449, -0.06585278]], dtype=float32)
>>> f.b.data
array([ 0.,  0.], dtype=float32)

この辺りで、内部の仕様を知らずに実装していてると、理解不能になってしまいますね。
ちなみに、重み行列 W の初期化した覚えがないにも関わらず、値を有しているのは、chainerの仕様上、Linearのリンクを宣言した時点で、ランダムに初期値を振られているからだそうです。

したがって、chainerの公式ドキュメントにあるのですが、よく使用する形式だとこうなります。

>>> f = L.Linear(3, 2)
>>> x = chainer.Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
>>> y = f(x)
>>> y.data
array([[ 1.15724015,  0.43785751],
       [ 3.0078783 ,  0.80193317]], dtype=float32)

3次元のベクトルであったxが線形結合により、2次元のyに変換されていることがわかると思います。
このとき、内部で重み行列Wに自動的に初期値が割り振られているので、この計算がエラーを吐かずにできています。

一応、確認のため、重みの値を見てみると、確かに初期値が振られていました。

>>> f.W.data
array([[-0.02878495,  0.75096768, -0.10530342],
       [-0.26099312,  0.44820449, -0.06585278]], dtype=float32)

>>> f.b.data
array([ 0.,  0.], dtype=float32)

つぎに、前章で学んだ勾配の計算を行っていきます。
chainerの公式ドキュメントにかなり強調して注意書きが書かれているのですが、各勾配の値は計算するたびに、蓄積されて(accumulated)いきます。
そのため、通常は各勾配の値を計算する前に、以下のメソッドで勾配の値を0に初期化する必要があります。

>>> f.zerograds()

勾配の値が正しく初期化されているか確認します。

>>> f.W.grad
array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)

>>> f.b.grad
array([ 0.,  0.], dtype=float32)

それでは、各勾配の値を計算してみましょう。

>>> y.grad = np.ones((2, 2), dtype=np.float32)
>>> y.backward()
>>> f.W.grad
array([[ 5.,  7.,  9.],
       [ 5.,  7.,  9.]], dtype=float32)
>>> f.b.grad
array([ 2.,  2.], dtype=float32)

ちゃんと計算できていますね。

連鎖したモデルを構築 (Write a model as a chain)

前章で明示的に定義したモデルを多層に展開していきます。

>>> l1 = L.Linear(4, 3)
>>> l2 = L.Linear(3, 2)

一応、それぞれのモデルの重みを確認しておきましょう。

>>> l1.W.data
array([[-0.2187428 ,  0.51174778,  0.30037731, -1.08665013],
       [ 0.65367842,  0.23128517,  0.25591806, -1.0708735 ],
       [-0.85425782,  0.25255874,  0.23436508,  0.3276397 ]], dtype=float32)

>>> l1.b.data
array([ 0.,  0.,  0.], dtype=float32)

>>> l2.W.data
array([[-0.18273738, -0.64931035, -0.20702939],
       [ 0.26091203,  0.88469893, -0.76247424]], dtype=float32)

>>> l2.b.data
array([ 0.,  0.], dtype=float32)

上記により、各モデルの構造を定義しました。
つぎに、それらの構造を定義したモデルがどのように接続されているかといった全体の構造を明示していきます。

>>> x = chainer.Variable(np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32))

>>> x.data
array([[ 1.,  2.,  3.,  4.],
       [ 5.,  6.,  7.,  8.]], dtype=float32)

>>> h = l1(x)

>>> y = l2(h)

>>> y.data
array([[ 1.69596863, -4.08097076],
       [ 1.90756595, -4.22696018]], dtype=float32)

これらを再利用可能にするために、公式ドキュメントでは以下のようにクラスを作成することを推奨しています。

MyChain.py
# -*- coding: utf-8 -*-
from chainer import Chain
import chainer.links as L

class MyChain(Chain):

    def __init__(self):
        super(MyChain, self).__init__(
            l1 = L.Linear(4, 3),
            l2 = L.Linear(3, 2) )

    def __call__(self, x):
        h = self.l1(x)
        return self.l2(h)

最適化 (Optimizer)

つぎに、Neural Networkモデルの持つ重みの最適化を行います。
この重みの最適化手法にはいくつかあるのですが、正直どれを使えば良いといった基準が明確にあるわけではなさそうなので、ここでは、確率的勾配降下(SGD; Stocastic Gradient Descent) 法を使用します。
最適化手法による性能の違いは CNNの学習に最高の性能を示す最適化手法はどれか で解説して頂いています。

>>> model = MyChain()
>>> optimizer = optimizers.SGD()  # 最適化手法をSGDに指定
>>> optimizer.setup(model)
>>> optimizer
<chainer.optimizers.sgd.SGD object at 0x10b7b40d0>

このときのoptimizer.setup(model)により、モデルの持つパラメータの情報を渡すことができます。

公式ドキュメント内に最適化の方法にも2通りあると書かれています。
1つ目の方法では、勾配の値を手動で計算することになっており、勾配の手計算はなかなか大変です。
そのため、特殊な場合を除き、勾配を自動的に計算するといったもう1つの方法を使いましょう。
自動的に計算させる場合は、損失関数 (loss function) を予め定義しておく必要があります。

実数を扱う場合は最小二乗法の2ノルムの総和を最小化する問題として定義できますし、交差エントロピーの最小化問題として定義することもよくあるようです。
損失関数については、誤差逆伝播法のノート で様々な種類を解説していただいています。

損失関数
def forward(x, y, model):
    loss = ... # 各自で損失関数を定義
    return loss

今回は損失関数を計算する forward 関数に引数 x, y, model を取るように仮定しています。
このような損失関数を定義すると、以下のようにして、パラメータの最適化を行ってくれます。

optimizer.update(forward, x, y, model)

Chainerで非線形回帰をためそう

今回のゴール

下の画像のように、sin関数をバッチリ捉えることができるような非線形回帰モデルを構築します。

ゴール.png

非線形回帰モデルを構築

プログラムの全体像

MyChain.py
# -*- coding: utf-8 -*-
from chainer import Chain
import chainer.links as L
import chainer.functions as F

class MyChain(Chain):

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

    def predict(self, x):
        z1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(z1))
        return self.l3(h2)
example.py
# -*- coding: utf-8 -*-

# 数値計算関連
import math
import random
import numpy as np
import matplotlib.pyplot as plt
# chainer
from chainer import Chain, Variable
import chainer.functions as F
import chainer.links as L
from chainer import optimizers
from MyChain import MyChain

# 乱数のシードを固定
random.seed(1)

# 標本データの生成
#   真の関数としてsin関数を
x, y = [], []
for i in np.linspace(-3,3,100):
    x.append([i])
    y.append([math.sin(i)])  # 真の関数
# chainerの変数として再度宣言
x = Variable(np.array(x, dtype=np.float32))
y = Variable(np.array(y, dtype=np.float32))

# NNモデルを宣言
model = MyChain()

# 損失関数の計算
#   損失関数には自乗誤差(MSE)を使用
def forward(x, y, model):
    t = model.predict(x)
    loss = F.mean_squared_error(t, y)
    return loss

# chainerのoptimizer
#   最適化のアルゴリズムには Adam を使用
optimizer = optimizers.Adam()
# modelのパラメータをoptimizerに渡す
optimizer.setup(model)

# パラメータの学習を繰り返す
for i in range(0,1000):
    loss = forward(x, y, model)
    print(loss.data)  # 現状のMSEを表示
    optimizer.update(forward, x, y, model)

# プロット
t = model.predict(x)
plt.plot(x.data, y.data)
plt.scatter(x.data, t.data)
plt.grid(which='major',color='gray',linestyle='-')
plt.ylim(-1.5, 1.5)
plt.xlim(-4, 4)
plt.show()

標本データの生成

今回の非線形回帰モデルを構築するための教師データを生成します。
今回は1入力1出力のsin関数を使用します。

# 標本データの生成
#   真の関数としてsin関数を
x, y = [], []
for i in np.linspace(-3,3,100):
    x.append([i])
    y.append([math.sin(i)])  # 真の関数
# chainerの変数として再度宣言
x = Variable(np.array(x, dtype=np.float32))
y = Variable(np.array(y, dtype=np.float32))

モデルの定義

Deep LearningのモデルをChainerにより構築します。
今回は入力層、隠れ層1、隠れ層2、出力層の4層構成にしました。
ノードの数は適当に決めました(この辺りは前回も書きましたが、経験と勘です)。
興味のある方は、ここの値を編集してみると良いと思います。
隠れ層が2層ある理由は、1層で回帰してみると、うまく特徴を捉えられていなかったため、もう一層増やしたといったところです。

MyChain.py
# -*- coding: utf-8 -*-
from chainer import Chain
import chainer.links as L
import chainer.functions as F

class MyChain(Chain):

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

    def predict(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)

ポイントとしては、活性化関数にreluを使用しているところでしょうか。
少し前だと、この活性化関数にシグモイド関数を使用することが定番でしたが、最近では、誤差逆伝播法でパラメータを学習させる際に、後ろのそうに行くほど学習率が下がっていく問題を回避するために、reluをよく使うそうです。
この辺りは、私自身も感覚でしか知らないので、もう少し勉強が必要です。
活性化関数については、他にも色々と解説記事があるので、そちらをどうぞ。
参考:【機械学習】ディープラーニング フレームワークChainerを試しながら解説してみる。

未学習の状況で確認

全く学習させていない状況でどうなっているかを確認してみました。
最終的に出てくる結果だけでなく、途中経過も見ることで、全体感を把握できるようになると思っています。

## NNモデルを宣言
model = MyChain()

# プロット
t = model.predict(x)
plt.plot(x.data, y.data)
plt.scatter(x.data, t.data)
plt.grid(which='major',color='gray',linestyle='-')
plt.ylim(-1.5, 1.5)
plt.xlim(-4, 4)
plt.show()

スクリーンショット 2016-08-08 15.49.58.png

未学習の状況だと、真の関数の特徴を全く捉えられていないといったことがわかるかと思います。

パラメータを学習させる

パラメータを学習させるにあたり、まず損失関数を定義します。
今回は自乗誤差 (MSE; Mean Squared Error) を損失関数として使用します。

{\rm MSE} = \dfrac{1}{N} \sum_{n=1}^{N} \left( \hat{y}_{n} - y_{n} \right)^{2}

※ $N$: サンプル数、 $y_{n}$: $n$ 番目の出力変数、$\hat{y}_{n}$: $n$ 番目の出力変数の推定値
mnistの例だと交差エントロピー関数を使用されていますね。

# 損失関数の計算
#   損失関数には自乗誤差(MSE)を使用
def forward(x, y, model):
    t = model.predict(x)
    loss = F.mean_squared_error(t, y)
    return loss

この損失関数を定義しておくことにより、Chainerでは optimizer の勾配計算を自動的に行うことができます。

# chainerのoptimizer
#   最適化のアルゴリズムには Adam を使用
optimizer = optimizers.Adam()
# modelのパラメータをoptimizerに渡す
optimizer.setup(model)
# 勾配の更新
optimizer.update(forward, x, y, model)

これで基本的な流れはおしまいです。

学習を繰り返す

さきほどの optimizer.update() を何度か繰り返すことにより、良いパラメータへと収束していきます。
今回は教師データを同じもので何度も学習させていますが、本来は、バッチデータとして、標本集団の中からいくつか取り出し、それらを教師データとして学習させ、次のサイクルでは別の標本をバッチデータとして使用するといった流れです。

# パラメータの学習を繰り返す
for i in range(0,1000):
    loss = forward(x, y, model)
    print(loss.data)  # 現状のMSEを表示
    optimizer.update(forward, x, y, model)

スクリーンショット 2016-08-08 16.16.36.png

学習を繰り返すごとに、自乗誤差が小さくなっていることがわかると思います。
これで学習を終えれば、非常になめらかに関数を近似出来ました。

ゴール.png

参考

1.Chainerの公式リファレンス
 色々と日本語で書いてあるものもありましたが、バージョンの変更などで対応できない部分に遭遇することが多く、英語ですがこちらが一番安定していました。
2.CNNの学習に最高の性能を示す最適化手法はどれか
3.誤差逆伝播法のノート