読者です 読者をやめる 読者になる 読者になる

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をコード化したものです。

f:id:Hi_king:20150627194056p:plain

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層に。

f:id:Hi_king:20150627194119p:plain

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使っ他問題も解いてみたので、次回の記事に書きたいと思います。

ソースコード全体

gist.github.com