2017.04.03 Mon |
Chainer入門その2
今回も前回の続きのchainer入門をやっていきましょう。
前回の記事はこちらになります。開発環境設定やNumpy入門がまだな方はこちらからどうぞ。
http://ritsuan.com/blog/5914/
それでは今回は、chainerに入門していきたいと思います。
参考資料は、前回同様、
chainer tutorial
http://docs.chainer.org/en/latest/tutorial/basic.html
chainer playground
https://play.chainer.org/
です。
それでは参りましょう。
まず大枠から始めましょう。
機械学習のステップは以下の3つからなり順番に説明していきます。
1,学習対象のモデルを定義する
2,目的関数を定義する
3,目的関数を最適化することで,モデルを学習する
1,学習対象のモデルを定義する
Pythonプログラムでいうと,学習対象のモデルはクラスのメソッドであり,パラメータはメンバ変数に相当します。
例えば次の例を見てみましょう。
クラスFはパラメータ aとbを持ち、関数として、ax+bを表します。
#インスタンスの生成
#クラスFのaに3.0, bに1.0を代入して3.0x+1.0という関数を生成
f = F(3.0, 1.0)
#3x+1にx=1.0を代入
print(f(1.0))
#3x+1にx=2.0を代入
print(f(2.0))
Pythonでの、classや初期化メソッド_init_や特殊メソッド_call_を知らない方は、
みんなのPythonの9章を参照ください。
https://www.amazon.co.jp/%E3%81%BF%E3%82%93%E3%81%AA%E3%81%AEPython-%E7%AC%AC4%E7%89%88-%E6%9F%B4%E7%94%B0-%E6%B7%B3/dp/479738946X
ということで、学習対象のモデルはクラスのメソッドであり,パラメータはメンバ変数というお話でした。
では次に”Linkオブジェクト”について、お話します。
Linkは最重要と言っていいくらい重要なので覚えてくださいね!
・Chainerでは学習可能なモデルをLinkとよびます。
・ディープラーニングで利用される代表的なLinkは chainer.links でサポートされています。
・自分で新しいLinkを作ることもできます。
この chainer.links を L として使えるようにします。
from chainer import links as L
・最も基本的なLinkはLinearとよばれるLinkです。
・Linearは全ての入力と出力がつながっているようなニューラルネットワークを表します。
・Linearはニューラルネットワークの文脈では総結合層,数学の用語では線形変換,アフィン変換とよびます。
次の例では5個のユニットから,2個のユニットへの変換を表します。
この lin はLinkオブジェクトですが,次のように関数呼び出しをすることができます。
#np.onesで入力データを作成し、Variable関数にかける
x = Variable(np.ones((3, 5), dtype=np.float32))
#linの関数呼び出し
y = lin(x)
#結果の出力
y
ちなみにnp.ones((3,5),dtype=np.float32)は単精度浮動小数点型で、全成分が1の3行5列の行列を表します。
まとめると以下のようになります。
Variable関数が実はかなりポイントでして、順計算を定義するだけで、勝手に逆計算式を作ってくれるものです。後ほど詳しく書きます。
ちなみに入力情報を変えてみると以下のようになります。
入力情報
ちなみにですが、上記例のLinearは,5次元のベクトルから2次元のベクトルへの線形変換を表します。線形変換(アフィン変換)は次のように表される変換です。
f(x;θ)=Wx+b
θ=(W,b)
Functionオブジェクト
Chainerでもう一つ重要なオブジェクトとしてFunctionがあります。これも最重要なのでしっかり覚えてくださいね!
・FuncitonはLinkとは違って,学習可能なパラメータを持ちません。
・学習によって挙動を変えません。
・ディープラーニングで利用されている代表的な関数は chainer.functions で定義されています。
・自分で新しいFunctionを作ることもできます。
以降では,この chainer.functions をFとして使えるようにします。
from chainer import functions as F
例えば、DeepLearningの繁栄初期によく使われていたReLU(xというスカラーの入力に対して、xが負なら0、xが非負ならxを返す関数)は以下のように実装されます。
from chainer import functions as F
y1 = F.relu(x)
ちなみにですが、去年5万部を売り上げたベストセラー作品、
“ゼロから作るDeep Learning―Pythonで学ぶディープラーニングの理論と実装”は、DeepLearning系をやるなら必要な前提知識なので読んでおきましょう。
https://www.oreilly.co.jp/books/9784873117584/
これらのLinkとFunctionを組みわせて複雑な関数を作ることができます。
例えば,前の例のLinearを適用した後にReLUを適用した結果は次のように計算されます。
LinkとFunctionを組み合わせて,学習対象のモデルを実際に作ってみましょう。
三層からなるニューラルネットワーク(Multiple Layer Perceptron)の例をあげます。
Perceptronに関しては、Google検索していただくか、先ほどご紹介した、ゼロから作るDeepLearingの本をご参照ください。
赤いL.LinearがLinkで、ピンク色のl1,l2,l3がパラメータ、青いF.reluがFunctionになります。l1→F.relu→h1→l2→F.relu→h2→l3→returnという流れになっています。最後は、F.reluをかませていないのが重要です。かませると余計な制約をかけていることになります(softmaxの定義域は負を含む実数です)ので注意してください。
このMLPは,三つのLinear(l1, l2, l3)を学習可能なパラメータとして持ち,__call__でそれらのパラメータを利用して結果を計算します。
L.Linear の第一引数にはNoneを指定することで実際の入力からユニット数を自動で設定してくれます。
実際に一通りまとめて書くと以下のようになります。
以上のように、
(1)Linkを使って学習対象のパラメータを定義し,
(2)次にそれらを使って順計算を定義する
ことで学習対象のモデルを定義できます。
以上で機械学習の手順の
1,学習対象のモデルを定義する
2,目的関数を定義する
3,目的関数を最適化することで,モデルを学習する
のうち一つ目が終わりました。
では二つ目に行きましょう。
2,目的関数を定義する、です。
教師あり機械学習のうち、分類を考えます。
学習したいモデルは入力xが与えられた時に出力yの条件付確率p(y|x)を出力してくれるようなモデルです。
このようなカテゴリ値に対する確率分布をモデル化するにはSoftmaxを利用します。
Chainerではchainer.functionsでsoftmax関数が定義されているのでそれを使いましょう。
例えば,入力xを何らかのモデルで変換しカテゴリ種類数と同じ次元数を持つベクトルtを作ります
次にベクトルtをsoftmaxを使って確率分布に変換します。どういうことかといいますと、例えば、犬、猫、馬を成分に持つカテゴリ変数があったとしましょう。犬は(1,0,0),猫は(0,1,0),馬は(0,0,1)と表されるとしましょう。例えば、入力xにより、t=(5,3,2)と出てきたとしたら、softmaxにより(5/10,3/10,2/10)とするイメージです(すべて正かつ和が1)。正確にはtを(e^5/(e^5+e^3+e^2),e^3/(e^5+e^3+e^2),e^2/(e^5+e^3+e^2))としてtの成分に負の数が出現しても対応できるようにします。
実際の実装は以下のようになります。
t = model(x)
y = F.softmax(t)
学習を行うために,入力と正解の出力のペアからなるn個のデータD={(x1,y1),(x2,y2),,,,(xn,yn)}
を用意します。
i=1,2,3,,,nとして、
p(yi|xi)が最大になるときに最小となる目的変数を設定すればいいわけですが、先人がすでに良いものを作っていまして、クロスエントロピー損失関数という有名なものがあります。chainerでは、softmax_cross_entropyという関数で実装されています。softmax_cross_entropy関数は、Softmaxを適用した後に,クロスエントロピー損失関数を適用する関数なので、softmax処理はすでに組み込まれています。
実装は以下のようになります。
学習の時に、入力と正解の出力のペアからなるデータを与えた時の処理です。
#xは入力, tは正解の出力
h = MLP(x)
loss = F.softmax_cross_entropy(h, t)
推定する値が連続値である回帰問題の場合は誤差2乗和を使います。
Chainerでは mean_squared_error として用意されています。
実際の実装は下記のようになります。
h = MLP(x)
loss = F.mean_squared_error(h, t)
損失関数のいくつかは chainer.functions 内で定義されています。
自分で新しい損失関数を定義することもできます。
以上で機械学習の手順の
1,学習対象のモデルを定義する
2,目的関数を定義する
3,目的関数を最適化することで,モデルを学習する
のうち2つ目が終わりました。
次に3つ目の目的関数を最適化する、に移りたいと思います。
目的関数が最大になるようなパラメータを求めたいわけですが、目的関数は、普通、パラメータがいくつもあり複雑な形をしているので簡単には、最小値を求めることができません。なので、普通以下のように勾配降下法で解かれます。
1,現在の位置x(t)から目的関数の値が最も急激に下がりそうな方向を調べる
この方向を勾配と呼び,-v(t)と書く。
2,その勾配にしたがって現在の位置から少し動かす: x(t+1)=x(t)-a(t)*v(t)
この時のステップ幅α(t)>0 を更新律と呼ぶ。
3,1-2を関数の値が変わらなくなるまで繰り返す
勾配降下法において,最も大変なのが勾配(-v(t))の推定です。ディープラーニングの場合は一番急激に下がっている方向を探すために,誤差逆伝播法(back propagation)を利用し推定します。誤差逆伝搬法とは、出力から入力に向かって,目的関数の出力についての勾配を伝播させていくことで効率的に勾配を求める手法です。 誤差逆伝搬法の計算量は順計算の計算量とほぼ同じであり効率的に勾配を求めることができます。
chainer.Variableはこの誤差逆伝搬法を実現するために必要な情報を記録する仕掛けが入っています。
Variable関数の説明は、以前”順計算を定義するだけで、勝手に逆計算式を作ってくれるもの”と書きました。これを詳しく説明すると、Variable を変数として順計算の計算手順を書いている時,Chainerは後で誤差逆伝播法ができるように内部で計算グラフを構築しており、例えば,前回の loss を目的関数とした場合,途中のパラメータ,入力についての勾配は, backprop という関数を呼び出すことで求めることができます。
#xは入力, tは正解の出力
h = MLP(x)
loss = F.softmax_cross_entropy(h, t)
loss.backprop()
勾配情報(-v(t))はこのlossの計算に関わった全てのVariable, Link の grad属性に格納されます。
勾配情報に基づきパラメータを更新する手法が chainer.optimizers にサポートされています。
代表的な最適化手法はSGD, RMSprop, Adamなどです。
最適化エンジンがどの学習可能な関数を目標とするかは setup で設定します。
#chainer.optimizersをimport
from chainer import optimizers
#最適化手法として、Adamを選択
opt = optimizers.Adam()
#最適化手法がどの関数を最適化の目標とするかの指定
opt.setup(model)
#勾配を求める
loss.backprop()
#その勾配情報を元に最適化
opt.update()
ここで、確率的勾配降下法の説明を軽くします。
今まで勾配降下法を使うと言ってきましたが、毎回勾配を求めるたびにデータを全て調べるのは計算コストが大きすぎます。 そのため,データ全体を使わずにデータの一部だけを利用し勾配の推定値を求め, それを利用しパラメータを更新することが一般的に行われています。これを確率的勾配降下法(Stochastic Gradient Descent:SGD)とよびます。この勾配推定に使うためにサンプリングされた学習データをミニバッチとよび,その個数をミニバッチサイズと呼びます。 ミニバッチサイズはだいたい32〜1024程度の値が利用されます。
以上で機械学習の手順の
1,学習対象のモデルを定義する
2,目的関数を定義する
3,目的関数を最適化することで,モデルを学習する
のすべてが終わりました。
それでは、以上のすべての知識をすべてつなぎ合わせて、三層のMLPによるMNISTデータの分類をやってみましょう。MNISTは手書き文字データセットであり、70000枚の28*28のグレイスケール画像から構成されていて、それぞれの画像に0〜9の数字が書かれています。 このデータセットを通常は60000の学習データと,10000のテストデータに分けて使います。実際下記コマンドでは、デフォルトで分けられています。また、以降では各画像を28*28の画素を並べた784のグレイスケール値がならんた784次元のベクトルとして扱います。
MNISTデータセットのダウンロードは次の datasets.get_mnist() を呼び出すことで実行されます。
※2017年4月3日現在chainerPlaygroundでは、datasetsがdatasetになっており(from chainer import datasetやdataset.get_mnist())、それは間違いですので、ご注意ください。また、import playgroundはあくまで、playground上でのみ有効なコマンドなのでご注意ください。
from chainer import datasets
#60000の学習データと,10000のテストデータに分けられる
train, test = datasets.get_mnist()
これらのデータは chainer.TupleDataset で構成されており,各サンプルが画像とそのラベル(0〜9)のタプルから構成されています。 例えば,train[100]は100番目のデータの画像とラベルからなるタプルを返します
x, y = train[100]
print x
print y
以下自分の環境で実行した結果(xは784次元のベクトル(浮動小数点値),yがラベル(整数値))
ちなみに、playgroundでは、playground.print_mnistが用意されており、実際のデータを視覚的に把握することができます。
さて、次は、先ほど挙げた、3層のMLP(Multiple Layer Perceptron)を使用するので、中身を詳しく見ていきましょう。
ここはplaygroundの記述をそのまま引用します。
”ニューラルネットワークのモデルを定義するオブジェクトは chainer.Chain(以降 Chain )を継承します。 Chain を継承することで,このモデルを保存したり読み込んだりすることができます。
モデルでは初期化時にモデル内で利用するパラメータ付き関数であるLinkを登録します。
上記の例では Linear である l1, l2, l3 を登録しています。 Linear は線形変換であり,初期化引数として入力次元数と出力次元数をうけとります。 Linear の入力次元数に None を指定した時は,それが最初に呼び出された時,次元数を引数から推定してくれます。”
Chain においてLinkの登録は、次のように add_link(name, link) で登録することもできます。
順計算は,多くの場合 __call__ メソッドで定義します。 さきほど登録した l1, l2, l3 を使って入力 x から3回線形変換と2回ReLUを適用して結果を返す部分が以下です。
順計算は __call__ で定義する必要は必ずしもありません。例えば,2層目の途中の中間結果を返すメソッドを次のように定義し使うこともできます(forward_with_two_layersの名前自体は何でもよく、重要なのはreturnでself.l2(h1)を返しているところ)。
以上のように作成したMLPを分類器として使うには L.Classifier を使ってモデルを作ります。
Classifier はデフォルトでは分類器softmax,学習時の損失関数はsoftmaxクロスエントロピー損失を使います。 Classifier が引数としてとるモデルは __call__() で順計算が定義されていることを想定しています。
#L.Classifierを使ったモデルの作成
#784次元のベクトル(各画像データ)と0-9までの数字10個を指定
model = L.Classifier(MLP(784, 10))
それでは、MNISTデータの学習を行っていきたいと思います。
まずはデータのロードから。今回は、時間の短縮のため、学習用(訓練用)もテスト用も1000個抽出して使用します。
今回は、バッチサイズを100にするので、モデルの作成は以下のようにして行います。
model = L.Classifier(MLP(100, 10))
次にIteratorを作成します。
データセット上の操作を抽象化する Iterator を用意します。 Iterator は構築時にデータセットを引数として指定すると,そのデータセットに対する Iterator を返します。 引数として,batch_size は,一度のアクセスでいくつ同時に読み込むか, shuffle はアクセスの際にランダムにアクセスするかどうかを指定します。
#バッチサイズ(一度のアクセスでいくつ同時にどれくらい読み込むか)に指定
batchsize = 100
#Iteratorのセットアップ(repeatは繰り返し数、shuffleはアクセスの際ランダムにするかどうかの指定)
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize,repeat=False, shuffle=False)
パラメータの最適化を担当する Optimizer を用意します。 ここではAdamを用います。Adamは幅広い学習問題に対して安定的に学習できる手法です。
#Adamの指定
opt = chainer.optimizers.Adam()
#setup()内でChainまたはLinkを指定
opt.setup(model)
次は、パラメータ更新を担当する Updater を用意します。 先ほど用意したIterator,最適化を担当する Optimizer ,そしてどのデバイスで 実行するのかを指定します。device=-1はCPUを使うことを表します。
updater = training.StandardUpdater(train_iter, opt, device=-1)
最後に学習ループを担当する Trainer を用意します。
#学習回数の指定(今回は5エポック(5回データセットを走査する))
#workディレクトリにresultというフォルダを用意しておきましょう。
epoch = 5
trainer = training.Trainer(updater, (epoch, ‘epoch’), out=’result’)
Trainerには様々な拡張機能があります。
#テストデータで学習器性能を評価をする
trainer.extend(extensions.Evaluator(test_iter, model, device=-1))
#trainerのrunを呼び出すことで学習させる。
trainer.run()
#学習の結果を確認。
#ランダムに選んだテストデータ1件に対する予測を出力。
x, y = test[np.random.randint(len(test))]
pred = F.softmax(model.predictor(Variable(x.reshape((1, 784))))).data
print(“Prediction: “, np.argmax(pred))
print(“Correct answer: “, y)
以上を自分のlaptopでやってみたところ、9というデータが9と判別されておりうまくいっていました!
ちなみに、playgroundで、create model以降、以下のコマンドで実行したら、以下の結果が出ました。
うまくできたようですね。
# create model
model = L.Classifier(MLP(100, 10))
# load dataset
train_full, test_full = chainer.datasets.get_mnist()
train = datasets.SubDataset(train_full, 0, 1000)
test = datasets.SubDataset(test_full, 0, 1000)
# Set up a iterator
batchsize = 100
train_iter = chainer.iterators.SerialIterator(train, batchsize)
test_iter = chainer.iterators.SerialIterator(test, batchsize,
repeat=False, shuffle=False)
# Set up an optimizer
opt = chainer.optimizers.Adam()
opt.setup(model)
# Set up an updater
updater = training.StandardUpdater(train_iter, opt, device=-1)
# Set up a trainer
epoch = 5
trainer = training.Trainer(updater, (epoch, ‘epoch’), out=’/tmp/result’)
trainer.extend(extensions.Evaluator(test_iter, model, device=-1))
# Run the trainer
trainer.run()
# Check the result
x, y = test[np.random.randint(len(test))]
playground.print_mnist(x)
pred = F.softmax(model.predictor(Variable(x.reshape((1, 784))))).data
print “Prediction: “, np.argmax(pred)
print “Correct answer: “, y
ということで、かなり長くなってしまったchainer入門終わりたいと思います。
chainer playgroundとchainer tutorialの6-7割の内容は網羅できたかなと思います。
鈴木瑞人
東京大学大学院 新領域創成科学研究科 メディカル情報生命専攻 博士課程1年
東京大学機械学習勉強会 代表
NPO法人Bizjapan Technology部門 BizXチームリーダー