はじめに
最近流行りのDeep Learningに関する入門記事です。
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.grad
の 8
という値が導き出されています。
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)
これらを再利用可能にするために、公式ドキュメントでは以下のようにクラスを作成することを推奨しています。
# -*- 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) を予め定義しておく必要があります。
詳しくは次回 「Deep Learning入門(2) - Chainerで非線形回帰を試そう -」 にて紹介していきますが、各自で損失関数を定義します。
実数を扱う場合は最小二乗法の2ノルムの総和を最小化する問題として定義できますし、交差エントロピーの最小化問題として定義することもよくあるようです。
損失関数については、誤差逆伝播法のノート で様々な種類を解説していただいています。
def forward(x, y, model):
loss = ... # 各自で損失関数を定義
return loss
今回は損失関数を計算する forward
関数に引数 x
, y
, model
を取るように仮定しています。
このような損失関数を定義すると、以下のようにして、パラメータの最適化を行ってくれます。
optimizer.update(forward, x, y, model)
参考
1.Chainerの公式リファレンス
色々と日本語で書いてあるものもありましたが、バージョンの変更などで対応できない部分に遭遇することが多く、英語ですがこちらが一番安定していました。
2.CNNの学習に最高の性能を示す最適化手法はどれか
3.誤差逆伝播法のノート
4.Deep Learning入門(2) - Chainerで非線形回帰を試そう -
おまけ
フォローお待ちしています!
Qiita: Carat 吉崎
twitter:@carat_yoshizaki
はてなブログ:Carat COOのブログ
ホームページ:Carat
機械学習をマンツーマンで学べる家庭教師サービス「キカガク」
「数学→プログラミング→Webアプリケーション」まで一気に学べる「キカガク」に興味のある方はお気軽にご連絡ください。