酒飲みによる機械学習日記

〜ただの酒好きが、なんとなく流行っている機械学習について語っていきます、たまに金融ネタも〜

【Chainer:1】y=ax+bのパラメータ学習

はじめに

普段からDeep LearningのフレームワークではChainerを使っているのですが、まだまだ知らない機能等、たくさんありそうなので、簡単なことを色々やっていって、ChainerそしてDeep Learingの知識を深めていけたらなと思っています。

初回の今回は、y=ax+bのパラメータa, bを指定して、学習をさせてみようと思います。

下の図は、slope(a)=2, intercept(b)=1として、学習させた時の様子です!学習を繰り返していって、30Epoch頃には、大体のパラメータ推定が完了していることがわかります。

f:id:nts-524:20181214073833p:plain

構成

以下の文章の構成です。

  1. 使用データ
  2. ネットワークの定義
  3. 学習アルゴリズム
  4. 使用したTrainerのExtensions
  5. 結果
  6. 最後に

1. 使用データ

皆さんご存知の通り、y=ax+bの傾き(slope)とy切片(intercept)を計算するには、以下の2つが必要です。

  • xの増加量
  • yの増加量

よって、neural networkのインプットには、(x, y)の組を2つ以上入れてあげる必要があります。なので、以下のような関数を定義しました。

def generate_data(slope=np.float32(SLOPE), intercept=np.float32(INTERCEPT)):
        x = np.random.randn(2).astype(np.float32)
        d = np.concatenate([x, x*slope+intercept])
        y = np.array([slope, intercept])
        return (d, y)

関数の引数のSLOPEとINTERCEPTを指定してあげると、(d, y)のタプルを返すはずです。

  • d: x, yの組を2つずつ持つnp.array
  • y: slopeとinterceptの値を持つnp.array

あとは以下のような感じで、データを1000個発生させました。

train = [generate_data() for i in range(1000)]
train_iter = SerialIterator(dataset=train, batch_size=args.batchsize, shuffle=True, repeat=True)

2. ネットワークの定義

全結合層1つだけのネットワークです。インプットのサイズは4((x,y)が2組のデータのため)、アウトプットのサイズはslopeとintercept分で2となります。今回はlossだけでなく、slopeとinterceptが正解の値に近づいていく様子も可視化するために、reporter.report()の機能も利用しています。

活性化関数にはF.relu()を利用していますが、今回の問題であれば、なくても普通に学習します。

class Net(Chain):
    def __init__(self):
        super(Net, self).__init__()
        with self.init_scope():
            self.ln1 = L.Linear(in_size=4, out_size=2)

    def __call__(self, x, y):
        # compute loss
        x = chainer.Variable(x)
        h = F.relu( self.ln1(x) )
        loss = F.mean_absolute_error(y, h)

        # set reporter
        reporter.report({'loss': loss}, self)
        reporter.report({'slope': h[0,0]}, self)
        reporter.report({'intercept': h[0,1]}, self)
        return loss

3. 学習アルゴリズム

Adamを使用しました。

4. 使用したTrainerのExtensions

基本的にはよく使われるものを使用しています。ちょっと加えているのは、reporter.report()で登録した、slopeとinterceptを可視化するために、PlotReportがlossのものと合わせて2つになっていることぐらいですかね。

trigger = (1, 'epoch')
trainer.extend(extensions.ProgressBar(update_interval=1))
trainer.extend(extensions.LogReport(trigger=trigger), trigger=trigger)
trainer.extend(extensions.PlotReport(
    ['main/loss'], 'epoch', file_name='loss.png', trigger=trigger
))
trainer.extend(extensions.PlotReport(
    ['main/slope', 'main/intercept'], 'epoch', file_name='slope_intercept.png', trigger=trigger
))
trainer.extend(extensions.PrintReport(
    ['epoch', 'iteration', 'main/loss', 'main/slope', 'main/intercept', 'elapsed_time']
), trigger=trigger)

5. 結果

lossの推移

lossがどんどん下がっていっていることがわかります。ほぼ0になっており、しっかり学習しています。

f:id:nts-524:20181214073838p:plain

slopeとinterceptの推移

正解はslope=2, intercept=1であり、ブレながらも、30Epoch学習する頃には、ほとんど正解になっていることがわかります。

f:id:nts-524:20181214073833p:plain

6. 最後に

今回は中学生の頃に誰もが一度は解いたことのある、y=ax+bのパラメータ推定をDeep Learningで解いてみました。学習に必要なデータ(今回であれば2組以上の(x, y)なんですが)を準備し、それに合わせてネットワークを定義して、Chainerで学習をするといった流れです。

結構簡単に学習してくれたので良かったです。時間がある方は、インプットを2組から3組に変えたら、よりlossがはやく0に近づくのか等、色々試すと勉強になるのかもしれません。

最後まで読んでいただきありがとうございます。

7. コード全文

コードや学習済みモデルは以下のリンクにあります。 github.com

network.py

# =-=-=-=-=-=-= module =-=-=-=-=-=-= #
# chainer
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import Chain
from chainer import reporter
import numpy as np

# =-=-=-=-=-=-= model =-=-=-=-=-=-= #
class Net(Chain):
    def __init__(self):
        super(Net, self).__init__()
        with self.init_scope():
            self.ln1 = L.Linear(in_size=4, out_size=2)

    def __call__(self, x, y):
        # compute loss
        x = chainer.Variable(x)
        h = F.relu( self.ln1(x) )
        loss = F.mean_absolute_error(y, h)

        # set reporter
        reporter.report({'loss': loss}, self)
        reporter.report({'slope': h[0,0]}, self)
        reporter.report({'intercept': h[0,1]}, self)
        return loss

train.py

# =-=-=-=-=-=-= module =-=-=-=-=-=-= #
import os, glob, argparse
import numpy as np
import chainer
from chainer.iterators import SerialIterator
from chainer.training.updaters import StandardUpdater
from chainer.training import Trainer
from chainer.training import extensions
from chainer.serializers import load_npz, save_npz
from network import Net

# =-=-=-=-=-=-= config =-=-=-=-=-=-= #
SLOPE = 2
INTERCEPT = 1

# =-=-=-=-=-=-= main =-=-=-=-=-=-= #
def main():
    # - Argparse - #
    parser = argparse.ArgumentParser()
    parser.add_argument('--epoch', '-e', type=int, default=100,
                        help='The number of epochs')
    parser.add_argument('--batchsize', '-b', type=int, default=32,
                        help='Batchsize in mini batch')
    parser.add_argument('--result', type=str, default='result/',
                        help='Path to saving directory')
    parser.add_argument('--resume', '-r', type=str, default='',
                        help='Resume the training from snapshot')
    args = parser.parse_args()

    if not os.path.exists(args.result):
        os.makedirs(args.result)

    # - Set data - #
    def generate_data(slope=np.float32(SLOPE), intercept=np.float32(INTERCEPT)):
        x = np.random.randn(2).astype(np.float32)
        d = np.concatenate([x, x*slope+intercept])
        y = np.array([slope, intercept])
        return (d, y)

    train = [generate_data() for i in range(1000)]
    train_iter = SerialIterator(dataset=train, batch_size=args.batchsize, shuffle=True, repeat=True)

    # - Load model - #
    model = Net()

    # - Set optimizer - #
    optimizer = chainer.optimizers.Adam()
    optimizer.setup(model)

    # - Define trainer - #
    updater = StandardUpdater(train_iter, optimizer, device=-1)
    trainer = Trainer(updater, (args.epoch, 'epoch'), out=args.result)

    # - Set extentions - #
    trigger = (1, 'epoch')
    trainer.extend(extensions.ProgressBar(update_interval=1))
    trainer.extend(extensions.LogReport(trigger=trigger), trigger=trigger)
    trainer.extend(extensions.PlotReport(
        ['main/loss'], 'epoch', file_name='loss.png', trigger=trigger
    ))
    trainer.extend(extensions.PlotReport(
        ['main/slope', 'main/intercept'], 'epoch', file_name='slope_intercept.png', trigger=trigger
    ))
    trainer.extend(extensions.PrintReport(
        ['epoch', 'iteration', 'main/loss', 'main/slope', 'main/intercept', 'elapsed_time']
    ), trigger=trigger)

    # - Resume - #
    if args.resume:
        load_npz(args.resume, trainer)

    # - Run trainer - #
    trainer.run()

    # - Save trained model - #
    model.to_cpu()
    save_npz( os.path.join(args.result, 'model.npz'), model )
    save_npz( os.path.join(args.result, 'optimizer.npz'), optimizer )

if __name__ == '__main__':
    main()