chainerでニューラルネットを学んでみるよ(chainerでニューラルネット2)
前回の記事ではchainerのインストールからサンプルコードを使って画像識別問題を解くところまでやりました。 hi-king.hatenablog.com
今回の記事では回帰・分類問題用のシンプルなニューラルネットの作り方をやろうと思います。andとxorの論理式を学習させます。chainerでの実装の学習と、あとニューラルネットの教育目的に使いやすいなーと思ったので。2層のニューラルネットまで段階をふんで解説してるんですが、プログラム読むほうが得意、って方は一番最後のコードを先に読んだほうがわかりやすいかもしれません。
追記(7/13)型チェック
chainer1.1.0から型チェックが入ったので(https://github.com/pfnet/chainer/pull/95)、識別にはfloat32を入力してint32を出力、回帰にはfloat32を入力してfloat32を出力、という型でデータを入れるようになりました。元の、型を曖昧に扱っているコードが動かなくなったので修正しました。(train()の、x=....astype(numpy.int32)とかの部分)
ネットワークの書き方
下記のコードは、andを学習するために作ったネットワークです。最も小さいサンプルと言っていいと思います。andは2項演算なので、2入力のニューロン、そして2クラス分類問題として解くためにそれを2つ用意してます。図のNNをコード化したものです。
class SmallClassificationModel(chainer.FunctionSet): def __init__(self): super(SmallClassificationModel, self).__init__( fc1 = chainer.functions.Linear(2, 2) ) def _forward(self, x): h = self.fc1(x) return h def train(self, x_data, y_data): x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False) y = chainer.Variable(y_data.astype(numpy.int32), volatile=False) h = self._forward(x) optimizer.zero_grads() error = chainer.functions.softmax_cross_entropy(h, y) error.backward() optimizer.update() print("x: {}".format(x.data)) print("h: {}".format(h.data)) print("h_class: {}".format(h.data.argmax())) model = SmallClassificationModel() optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9) optimizer.setup(model.collect_parameters())
chainerで、あるネットワークを記述するために必要な記述は主に3つ、層のリスト、層の結合、ロス関数です。
利用する層のリスト
initに記述している、fc1は一層目の(いまは一層しか作ってないけれど)ニューロン群の記述です。chainer.FunctionSetのinitに渡しているのは、こうすることでoptimizerでパラメータ学習する際によしなにやってくれるからです。 なお、chainerではネットワークは直列でなく、分岐や再帰してもよいので、層という言い方はおかしいかもしれません。関数とか、ニューロン群とか言えばいいでしょうか。いい名前有るのかしら。
層同士の結合
initでは使う層を定義しただけなので、それが相互にどう結合するか記述する必要があります。_forwardに記述しているのがそれです。今回は一層なので微妙ですが、例えば二層あればself.fc2(self.fc1(x))と言った感じです。具体的には後述する二層モデルのコードを見てください。
ロス関数
学習時に教師データとNNの出力の誤差を計算する関数です。error = chainer.functions.softmax_cross_entropy(h, y)
の部分ですね。ここでは、分類問題としているので、softmax_cross_entropyを使ってます。回帰だったら例えばユークリッド距離を使うとか、分類でも、クラスごとに重みを変えたいとか、まぁいろいろ考えられると思います。
学習!
## 上のコードのあとに以下を追加。そして実行 data_and = [ [numpy.array([0,0]), numpy.array([0])], [numpy.array([0,1]), numpy.array([0])], [numpy.array([1,0]), numpy.array([0])], [numpy.array([1,1]), numpy.array([1])], ]*1000 for invec, outvec in data_and: model.train(invec, outvec)
andのロジックを表すデータをとりあえず4*1000個入れてみます。なお、出力は2次元なんですが、正解データとして入れるのは、クラスのインデックス1次元なことに注意です。
... x: [[0 0]] h: [[ 5.30860519 -5.30860519]] h_class: 0 x: [[0 1]] h: [[ 2.06165457 -1.59285831]] h_class: 0 x: [[1 0]] h: [[ 0.9362464 -2.718467 ]] h_class: 0 x: [[1 1]] h: [[-2.31046915 0.9970448 ]] h_class: 1
最終的にロジックが学習できたと思います。
ここで使ってるlinearについて説明してなかったですが、これはy1 = (w1, w2)*(x1, x2) + b1
のw,bを学習するモデルなので、たとえばy_true = x1+x2、y_false = 1.5 とでもすればandの関数になりますね。
xorは学習できない
ここまで1層だけのNNだったので、つまらないなぁという感想だと思います。1層で学習できないロジックを例示してみましょう。先ほどと同じネットワークで以下のロジック、xorを学習してみます。
data_xor = [ [numpy.array([0,0]), numpy.array([0])], [numpy.array([0,1]), numpy.array([1])], [numpy.array([1,0]), numpy.array([1])], [numpy.array([1,1]), numpy.array([0])], ]*1000
x: [[0 0]] h: [[-0.00494588 0.00494588]] h_class: 1 x: [[0 1]] h: [[ 0.81032435 0.80043193]] h_class: 0 x: [[1 0]] h: [[ 0.34743322 0.33754217]] h_class: 0 x: [[1 1]] h: [[ 1.1429201 1.15281156]] h_class: 1
まさかの、真逆のロジックを学習してますね。まぁこれは1層のlinearのロジックでは線形分離可能な問題しか解けないからです。という訳で2層に。
class ClassificationModel(chainer.FunctionSet): def __init__(self): super(ClassificationModel, self).__init__( fc1 = chainer.functions.Linear(2, 2), fc2 = chainer.functions.Linear(2, 2) ) def _forward(self, x): h = self.fc2(chainer.functions.sigmoid(self.fc1(x))) return h def train(self, x_data, y_data): x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False) y = chainer.Variable(y_data.astype(numpy.int32), volatile=False) h = self._forward(x) optimizer.zero_grads() error = chainer.functions.softmax_cross_entropy(h, y) error.backward() print("x: {}".format(x.data)) print("h: {}".format(h.data)) print("h_class: {}".format(h.data.argmax())) model = ClassificationModel() optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9) optimizer.setup(model.collect_parameters())
fc2が追加されているのと、fc2に渡す前にシグモイド関数を通して二値化してます。
x: [[0 0]] h: [[ 1.72677757 -1.22870416]] h_class: 0 x: [[0 1]] h: [[-0.93671401 1.36127727]] h_class: 1 x: [[1 0]] h: [[-0.9401792 1.36309223]] h_class: 1 x: [[1 1]] h: [[ 0.85140373 -0.75365336]] h_class: 0
やったね。 ※稀に1000サンプルで学習が終わらないことがあるので、その場合はやり直すか、サンプル数増やしてください。
それ、回帰でできるよ
ここまで2クラス識別問題として解いてきましたが、xor関数やand関数は2入力1出力の関数と捉えるのが一般的かと思います。出力を1次元にしても、RMSEを誤差関数として用いた回帰問題にすることで解けます。
class RegressionModel(chainer.FunctionSet): def __init__(self): super(RegressionModel, self).__init__( fc1 = chainer.functions.Linear(2, 2), fc2 = chainer.functions.Linear(2, 1) ) def _forward(self, x): h = self.fc2(chainer.functions.sigmoid(self.fc1(x))) return h def train(self, x_data, y_data): x = chainer.Variable(x_data.reshape(1,2).astype(numpy.float32), volatile=False) y = chainer.Variable(y_data.astype(numpy.float32), volatile=False) h = self._forward(x) optimizer.zero_grads() error = chainer.functions.mean_squared_error(h, y) error.backward() optimizer.update() print("x: {}".format(x.data)) print("h: {}".format(h.data)) model = RegressionModel() optimizer = chainer.optimizers.MomentumSGD(lr=0.01, momentum=0.9) optimizer.setup(model.collect_parameters())
fc2の出力が1次元になってるのと、errorがmean_squared_errorになってます。
... : [[0 0]] h: [[ 0.0449773]] x: [[0 1]] h: [[ 0.93737204]] x: [[1 0]] h: [[ 0.93765563]] x: [[1 1]] h: [[ 0.09684665]]
出力が連続値になるので閾値入れて2値化する必要はありますけど、ちゃんと解けてますね。
感想
chainerで書くと、ニューラルネット書いてる、って感じになるので教育用にいいですね。CNN使っ他問題も解いてみたので、次回の記事に書きたいと思います。