のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

Go言語を触ったことのない人間がSensorBeeを使ってみた Part1

本日はさくっとSensorBeeを使ってみました。

皆さんこんにちは
お元気ですか。連休でほっとしています。

今回はSensorBeeと呼ばれるライブラリを紹介します。
Machine Learningまでは流石に長過ぎるので、word countまでやってみました。

What is SensorBee

sensorbee.io

SensorBeeは特徴として以下の内容を兼ね揃えます。

Stateful

機械学習に有用な構造化されていない情報を取得し、ユーザが定義した機械学習のモデルを動作させることができます。
また、ChainerやJubatusに加え、その他SensorBeeで記載できるQueryによっても作成することができます。

Expressive

命令やクエリはBQLで書かれています。BQLはパワフルな言語で簡単に習得でき、SQLに似ています(確かに)。
BQLはスキーマレスで、JSONにかなり近い、データ構造となってます。

LightWeight

SensorBeeはLightweightで軽量です。30MBも必要ないので、Raspberry Piのような
小型であっても動作します。 しかしながら、very smallなデバイスには搭載するには十分ではありません。
将来的には、このようなデバイスにも動作させたいんだそうな。

Install

goを持ってない人はgoからのインストールが必要となります。

Go(Mac)

Go自体のインストールと環境変数となります。

$ brew install go
$ export GOROOT=/usr/local/opt/go/libexec
$ export GOPATH=$HOME/go
$ export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

SensorBeeのインストール

SensorBeeのインストールは簡単です。

$ go get gopkg.in/sensorbee/sensorbee.v0/...

Tutorial

これだけではなんのことかまるでわからないので、Tutorialを実施してみます。
まずは、データの取得と初期化を行います。

データの取得と初期化

$ go get github.com/sensorbee/tutorial/wordcount
$ mkdir wordcount
$ cp $GOPATH/src/github.com/sensorbee/tutorial/wordcount/config/* ./wordcount/
$ build.yaml	sensorbee.yaml	   wordcount.bql

ビルド用のコマンドとしてbuild_sensorbeeを使う必要があります。

$ go get gopkg.in/sensorbee/sensorbee.v0/cmd/build_sensorbee
$ cd wordcount/
$ build_sensorbee

build.yamlの内容は以下のようになります。

$ cat build.yaml
plugins:
  - github.com/sensorbee/tutorial/wordcount/plugin

サーバの起動

これでSensorBeeを使う準備ができました。
早速、SensorBeeのサーバを起動してみましょう。

$ ./sensorbee run
INFO[0000] Setting up the server context                 config={"logging":{"log_dropped_tuples":false,"min_log_level":"info","summarize_dropped_tuples":false,"target":"stderr"},"network":{"listen_on":":15601"},"storage":{"uds":{"params":{},"type":"in_memory"}},"topologies":{}}
INFO[0000] Starting the server on :15601

試しにcurlを投げてみましょう。以下のようなレスポンスが返ってくるはずです。

$ curl http://localhost:15601/api/v1/runtime_status
{"gomaxprocs":4,"goroot":"/usr/local/opt/go/libexec","goversion":"go1.5.3","hostname":"{host name}-Pro.local","num_cgo_call":1,"num_cpu":4,"num_goroutine":13,"pid":44076,"user":"Tereka","working_directory":"/Users/Tereka/Programing/Software/SensorBee/wordcount"}

まずは、topologyと呼ばれるものが必要になります。topologyは
RDBMSのデータベースに近いそうです。まずは、これを作成しましょう。

./sensorbee topology create wordcount

出力されているStreaming情報はBQLと呼ばれる文法を使うことで様々なことをすることができます。
まずは、stream(ストリーム)からデータを取得してみましょう。

wordcount> CREATE SOURCE sentences TYPE wc_sentences;
wordcount> SELECT RSTREAM * FROM sentences [RANGE 1 TUPLES];

BQN

はじめに

BQNはストリーム間の関連性のデータ命令と関係性を示した命令があります。(あってる?)
ストリームの処理単位を決定することができます。
ストリームの処理単位は以下のような構文で実施することができます。

[RANGE n TUPLES] or [RANGE n SECONDS]. [RANGE n TUPLES]

例えば、以下のように書けます。

SELECT RSTREAM * FROM sentences [RANGE 1 TUPLES];

STREAMは3種類あります。

Stream 説明
RSTREAM 新しいtupleが到着した時に結果を更新する。(処理をする)
ISTREAM 到着したtupleのみ更新し、以前までの時刻は更新しない。(処理をしない)
DSTREAM 解説plz
SELECT文

まずは、SELECT文 SQLと殆ど同じ書き方です。

wordcount> SELECT RSTREAM name FROM sentences [RANGE 1 TUPLES];
{"name":"isabella"}
{"name":"isabella"}
{"name":"isabella"}
{"name":"jacob"}
{"name":"sophia"}
WHERE文

これも殆ど同じではないだろうか。
WHERE name = "sophia"の部分で、フィルタリングをかけています。

wordcount> SELECT RSTREAM * FROM sentences [RANGE 1 TUPLES] WHERE name = "sophia";
{"name":"sophia","text":"in velit commodo cillum cillum consequat proident dolore ut"}
{"name":"sophia","text":"in aute aliqua irure anim sit et"}
{"name":"sophia","text":"mollit sunt commodo id id commodo esse sit"}
{"name":"sophia","text":"cillum magna aute tempor eu velit"}
GROUP BY

SQLのGROUP BYと似たような使い方です。出力は名前が以前+今回で何回出てきたかをcountで出力しているようですね。

wordcount> SELECT ISTREAM name, count(*) FROM sentences [RANGE 60 SECONDS]
    GROUP BY name;
{"count":1,"name":"jacob"}
{"count":1,"name":"sophia"}
{"count":2,"name":"sophia"}
{"count":1,"name":"isabella"}
{"count":2,"name":"isabella"}

ストリームの作成

既にあるストリームから、新しいストリームを作成することができます。
sentencesを処理して新しいwordsと呼ばれるストリームを作成しています。

wordcount> CREATE STREAM words AS SELECT RSTREAM name, text AS word FROM wc_tokenizer("sentences", "text") [RANGE 1 TUPLES];
wordcount> SELECT RSTREAM * FROM words [RANGE 1 TUPLES];
{"name":"isabella","word":"proident"}
{"name":"isabella","word":"id"}
{"name":"isabella","word":"eu"}
{"name":"isabella","word":"laboris"}
{"name":"isabella","word":"sunt"}

これを使うと、以下のようにしてword countを実施することができます。

wordcount> SELECT ISTREAM word, count(*) FROM words [RANGE 60 SECONDS] GROUP BY word;
{"count":1,"word":"culpa"}
{"count":1,"word":"sunt"}
{"count":1,"word":"occaecat"}
{"count":1,"word":"ullamco"}
{"count":1,"word":"ut"}
{"count":1,"word":"consequat"}
{"count":1,"word":"minim"}
{"count":1,"word":"aute"}

それぞれのワードのカウント最大数と最小数を求めています。
また、以下のBQNによって、出現ワードの最大回数、最小回数を算出することができます。

wordcount> CREATE STREAM word_counts AS
    SELECT ISTREAM word, count(*) FROM words [RANGE 60 SECONDS]
    GROUP BY word;
wordcount> SELECT RSTREAM max(count), min(count)
    FROM word_counts [RANGE 60 SECONDS];
{"max":8,"min":8}
{"max":8,"min":5}
{"max":8,"min":5}
{"max":11,"min":5}
{"max":11,"min":5}

これで、ワードのカウントをSensorBeeで実施するチュートリアルを完走しました。
チュートリアルが半分残っているので時間に余裕があれば、Part2やります。
間違ってたら後で修正かけます。。

Kaggleのコンペティションで公開されている手法・ソースコードのリンクをまとめてみた Part2

皆さんこんにちは
お元気ですか。私は元気です。

今日は前回の以下のページからだいぶ更新が立ち、Kaggleのコンペ的にも多くの開催がありました。
そこで、新しいページでリンクを纏めてみました。
中にはインタビューやフォーラム、githubなど様々なものが混合しているのはお許し下さい。

nonbiri-tereka.hatenablog.com

Airbnb New User Bookings

2nd-GitHub - Keiku/kaggle-airbnb-recruiting-new-user-bookings: 2nd Place Solution in Kaggle Airbnb New User Bookings competition
こちらは日本人の方でKaggle Meetupでの発表資料があります。
sssslide.com

Neural Networkでの失敗経験やアンチパターンを語る

皆さんこんにちは
お元気ですか。私は元気です。

今日は珍しくNeural Networkを使っていく上での失敗経験について語ります。
学習の時に案外、失敗するのですが、だいたい原因は決まっています。そう大体は・・・
ということで、今回は失敗の経験、アンチパターンのようなものを書こうと思います。

Trouble1:学習時にNanを叩き出す。

こんな経験よくあるのでは無いでしょうか?
なぜかよくわからないけれども学習していると気がついたらNanになっていることがあります。

原因1 cross-entropy誤差を使っている。

まずは、cross-entropy誤差関数を見てみましょう。
(今回はTheanoのbinary cross entropyを取り上げます)

{ \displaystyle
crossentropy(t,o) = -(t\cdot log(o) + (1 - t) \cdot log(1 - o))
}

実はこの数式、nanを出す要因があります。対数に着目してみます。
対数は0を入力するとnanが返ってきて、計算不可能な値となります。
何らかの原因で、対数に0が入力され、計算がおかしくなっています。

この場合はsigmoid関数やnp.log1pの関数を使い、関数に一工夫が必要になります。
しかし、sigmoid関数についても、nanが出力される場合があります。

原因2 結果が小さすぎて、0と認識される。

原因1の事象はsigmoid関数を使っている場合でも発生します。
sigmoid関数は∞に大きくなれば0や1を計算することが可能です。
Neural Networkの計算において、sigmoid関数はfloat32の場合において、小さすぎて0と算出されます。
これが原因になっていることがあります。

以下のコードを書いて検証してみました。

import numpy as np
def sigmoid(z):
	return 1/(1+np.exp(-z))

sigmoid(-500) #7.1245764067412845e-218
sigmoid(-500).astype(np.float32) #0.0

原因3 重みがあらぬ方向へ学習する。

回帰式を計算した時によくあることなのですが、
重みが凄い方向へ学習してそのままinfを叩き出し、nanになることがあります。

Trouble2:収束しない

作った関数が収束しない、これもよくあることです。
大体原因は決まっている気がします。

原因1 学習率が高すぎる

学習率が高過ぎると重みがぶれ、学習ができません。
あまりにも遅ければ上げてみる、もしくはAdamなど、自動的に学習してくれる
学習法を使ってみると良いと思います。

chainerのMNISTのexample(
chainer/train_mnist.py at master · pfnet/chainer · GitHub)
に対して学習方法をSGDにして、学習率1000000000.0とした場合の標準出力
正当率があがらず、誤差もnanが出続けています。

epoch 1
graph generated
train mean loss=nan, accuracy=0.0988166666403
test  mean loss=nan, accuracy=0.0979999999329
epoch 2
train mean loss=nan, accuracy=0.0987166666767
test  mean loss=nan, accuracy=0.0979999999329
epoch 3
train mean loss=nan, accuracy=0.0987166667202
test  mean loss=nan, accuracy=0.0979999999329

原因2 学習率が低すぎる

学習率が低すぎると、学習が遅くなります。
対策としては原因1と同じです。見なおしてみてください。

chainerのMNISTのexampleに対して学習方法をSGDにして、学習率0.0000001とした場合は以下になります。
縦軸が誤差、横軸がepoch数、で学習が殆ど進んでいないことが見てわかります。

f:id:tereka:20160310001616p:plain

対して学習率を0.1とした場合の誤差は以下のようになります。

f:id:tereka:20160310000439p:plain

個人的にこのパラメータは0.01周りを使っていればトラブルが少ないと考えています。

原因3 適切な誤差関数ではない

誤差関数がよくないといった失敗をやったことがあります。
基本的に誤差関数は、誤っている場合の誤差が大きければ、大きいほど、学習の進みがよくなります。
マルチラベルの場合最小二乗誤差ではなく、cross-entropyを使わなければ、収束がすごく遅いです。

因みにこれを私はcross-entropyと最小二乗誤差でやりました。

原因4 活性化関数を誤った

昔、回帰の問題を解いていました(100,300などを予測)が
盛大な誤差関数を出力するが、誤差落ちなかったことがありました。

モデルを調べた結果、なぜかsigmoid関数を使っていたことがありました。
全然減らないと思った時は最後の出力の活性化関数を見直すのも良いでしょう。

因みに負の値の予測が必要な時にReLuを使うのもよろしくありません。式を見れば一目瞭然ですが・・・

原因5 そもそも入力が誤っている

画像に変な処理をかけたことで、入力が誤っていることがあります。
例えばuint8に対してintの処理をすることで期待と異なる結果が返る場合があります。

2 / 100 #0
2 / 100.0 #0.02

Trouble3:Validation Scoreが低い

Neural Networkを作ったけれども精度が出ない、そんな失敗もあります。

原因1 過学習しているにも関わらず、気づかなかった。

Early Stoppingを実施しなかった時によく起こる問題だと思います。
Early Stoppingを実施しない場合は、一定の間は普通ですが、
あるときから、Validation Scoreが低下していきます。

原因2 与えるデータとラベルの1対1が誤っている

与えるデータとラベルを何らかの処理で取得している場合
1対1が間違っている可能性があります。

例えば、与えるデータとラベルをうっかりValidationの時に各々でシャッフルするなどあります。
あまりにも学習データと乖離しているときは、与えているデータが違っていないか疑ってみるのもよいでしょう。

Trouble4:正しい入力を与えたはずなのに、ライブラリから変なエラーを吐かれる場合

これは、どちらかというとライブラリ周りの話です。

原因1 モデル構築誤り

この場合はだいたい、入力誤りです。
連結している部分のfor文あたりが間違っている事が多いです。

例えば、連結が途切れているなどはこの場合に含まれるでしょう。

原因2 入力データのデータ形式誤り

ライブラリによって異なりますが、regressionとclassificationでは大体入力の方法が異なっています。
これに気づかずにやるとデータの形式にハマります。
時々、ConvolutionやDense Layer(Liner Layer)の対応マップが誤って出ることもあります。

Theano関連のライブラリだと以下の様な結果が出ることでしょう。

ValueError: Input dimension mis-match. (input[0].shape[1] = 2, input[1].shape[1] = 1)

Apply node that caused the error: Elemwise{sub,no_inplace}(Elemwise{Composite{tanh((i0 + i1))}}[(0, 0)].0, <TensorType(float64, matrix)>)
Inputs types: [TensorType(float64, matrix), TensorType(float64, matrix)]
Inputs shapes: [(50, 2), (50, 1)]
Inputs strides: [(16, 8), (8, 8)]

Python: keras shape mismatch error - Stack Overflowより

因みに大体のライブラリで回帰問題とクラス分類問題によって決まっており、
以下の入力をすれば、特段問題はないと思います。

[[1],[3],[4]] #regression
[1,2,3,4] #classification

まとめ

大体上記4つのトラブルに失敗の経験は含まれると思います。(私はそうでした)
皆さんの失敗経験がこれで減りますように。
他に失敗経験があったらぜひ教えて下さい。

Chainerにおけるグラフ構造をループで書いてみる。

皆さんこんにちは
お元気ですか。私は元気です。

実は私、Chainerでのfor文でLinkとして作成できることを知らず、
今の複雑なネットワークにChainer使いにくいと思っていましたが、以下にサンプルがあって
こうすれば複雑なネットワークも組めるんだ。みたいなところがわかりました。

Deep Residual Network definition by Chainer · GitHub

ChainerのLink構造について

以下のスライドにChainer version1.5のチュートリアル解説があります。
このうち今回で必要な情報はChain、Link、Functionが何を示しているのかです。

www.slideshare.net

chainerの関数 概要
chainer.Function 関数
chainer.Link パラメータ付き関数
chainer.Chain パラメータ付き関数集合

これに基づいて、関数集合を構築していけば良いといったところです。

※上記の関数はv1.5以降です。1.4以前では多分異なるので気をつけてください。

グラフ構造を容易にかくには

部分構造を構築する。

通常に計算できるパラメータ付き関数集合を作りつつ、Linkに突っ込めば実装可能です。
後は、linkから必要な情報を取り出し、forwardを構築するのみです。

今回はVGGNetを用いて実施してみます。
VGGNetの部分構造であれば、以下のように書くことができます。
例えば、ニューラルネットワークのとある箇所を部分的に書くと以下のようになります。

class RoopBlock(chainer.Chain):
    def __init__(self,n_in,n_out,stride=1):
        super(RoopBlock,self).__init__(
            conv1 = L.Convolution2D(n_in,n_out,3,stride,1),
            conv2 = L.Convolution2D(n_out,n_in,3,stride,1),
            conv3 = L.Convolution2D(n_out,n_in,3,stride,1)
        )

    def __call__(self,x,t):
        h = F.relu(self.conv1(x))
        h = F.relu(self.conv2(h))
        h = F.relu(self.conv3(h))

        return h

Chainと呼ばれる関数集合を構築することをまず行います。
これをforなどを使ってうまく書くと、VGGNetを以下のように記述することができます。

class VGGNet(chainer.Chain):
    def __init__(self):
        super(VGGNet,self).__init__()

        links = [("root0",RoopBlock(3,64))]
        n_in = 64
        n_out = 128
        for index in xrange(1,5,1):
            links += [("root{}".format(index),RoopBlock(n_in,n_out))]

            n_in *= 2
            n_out *= 2
        links += [("fc"),L.Linear(25088, 1000)]
        self.forward = links
        for link in links:
            self.add_link(*link)

        self.train = True

    def __call__(self, x, t):
        for name,func in self.forward:
            x = func(x)
        if self.train:
            self.loss = F.softmax_cross_entropy(x,t)
            self.accuracy = F.accuracy(x, t)
            return self.loss
        else:
            return F.softmax(x)

この書き方によるメリットは層を一つ増やしたいとなった場合に簡単に追加できることです。
add_linkを使うことで、パラメータをリンクとして登録しておきます。

ResNetを実際に実験するにあたって調べてて見つけた内容ですが、
この方法はチュートリアルにも掲載されていないので、あんまり見つけられないかもしれません。

参考文献は以下の通り。

GitHub - mitmul/chainer-cifar10: Various CNN models including Deep Residual Networks (ResNet) for CIFAR10 with Chainer (http://chainer.org)

Pythonで書ける便利な書き方を紹介します。

皆さんこんにちは
お元気ですか。私は元気です。

今日はPythonにおける便利記法を書いてみます。
他の言語しか使ったこと無い人もぜひ

※1/25間違いかと思われる箇所の加筆修正を実施

リストに関するイテレーション

enumerate(インデックスとオブジェクトのループ)

enumerateはiterate回数とオブジェクトを同時にループの記載をすることができます。
要素のi番目を取得してループを書くと以下の記載方法になります。

list = [1,2,3,4,5,6]

for i in xrange(len(list)):
    print i,list[i] 

enumerate関数を使うとより、直感的に記載することができます。

for i,number in enumerate(list):
    print i,number

zip(同時に2つのオブジェクトでループ)

zipを使用して固めて、2つ以上の配列をiterationする。
一つだけでなく、セットで固めることができます。

list,list2 = [1,2,3,4],[3,4,5,6]
for i in xrange(len(list)):
    print list[i],list2[i]

これをzipを使って書くと次のようになります。

list,list2 = [1,2,3,4],[3,4,5,6]
for num1,num2 in zip(list,list2):
    print num1,num2

因みにzipとenumerateを組み合わせ、何番目かつ、zipにより固めることができます。

list,list2 = [1,2,3,4],[3,4,5,6]
for index,(num1,num2) in enumerate(zip(list,list2)):
    print index,num1,num2

yield(途中で一旦返す)

yield記法は個人的にバッチ学習の時に使うことが多いです。
直感的にわかりにくいのですが、yieldの時に一回値を返すといったことをします。

def iteration_list(list,batch=2):
    for i in xrange(0,len(list),batch):
         yield list[i:i+batch]

for obj in iteration_list():
    print obj #list[i:i+batch]が表示される。

for-else(forで途中で停止しない場合の条件)

私もこれを作成している時に初めて知ったのですが、
forにelse文をつけることで、breakされなかった時の処理を記述することができます。

for i in xrange(10):
    if i == 100:
        break
else:
    print i #実行される

リスト内包表記

リストの内部にfor文を書き、短く書くことができます。
次に記載するのは0~9までの数字を持ったリストです。

print [i for i in xrange(10)]

また、条件式を使い特定の値を取得することができます。
通常のリストを構築し、appendを実施するより高速に動作するので、単純な処理であればこちらを記述してみましょう。

print [i for i in xrange(10) if i == 2]

Collections

Counter

import collections

counter_list = ["a","b","c","d","e","a","a"]
counter = collections.Counter(counter_list)
print counter
print counter["a"]

default dict(辞書のデフォルト値設定)

一言で説明すると辞書に存在しないキーの初期値を決めることができます。
通常であれば、以下の記述をすることになると思います。

dict = {}
if "a" in dict:
    dict["a"] += 1
else:
    dict["a"] = 0

しかし、collectionsを使うことで簡単に記述ができます。

import collections
dict = collections.defaultdict(int)
dict["a"] += 1

print dict

Sort(ソート)

sorted

sortedを使うと、色々なオブジェクトをソートすることができます。
昇順、降順は

list = [1,4,3,6,5,8]
print sorted(list)
print sorted(list,reverse=True)

出力

[1, 3, 4, 5, 6, 8]
[8, 6, 5, 4, 3, 1]

辞書をソートするコードは以下になります。

dict = {"a":1,"b":2,"c":1}
print sorted(dict.items(),key=lambda x:x[1],reverse=True)
[('b', 2), ('a', 1), ('c', 1)]

因みにソートしたいキーをkeyに選択すれば良いので、基本的にどんな
オブジェクトでもソートすることができます。

if文の書き方

Pythonでif文の条件は連結して書くことができます。
どういうことかといいますと、以下の例を見てみます。
これは、変数aが10より高い、かつ、100未満の時のプログラムにおける一般的な記載方法です。

a = 70
if a > 10 and a < 100:
    print "True"
else:
    print "False"

しかし、Pythonでは、以下のように条件を連結して書くことができます。

if 100 > a > 10:
    print "True"
else:
    print "False"

Fileに関する処理(with)

fileに関する処理は通常以下のように書き、自分の手でクローズしなければなりません。

f = open("test.txt")
#何らかの処理
f.close()

しかし、with statementの導入により、自分でクローズをする必要がなくなります。

with open("test.txt") as f
    print f.read()
    #何らかの処理

in(オブジェクトに存在しているかどうか)

文字列やリストに存在しているかどうか、調べるにはどうすればよいでしょう?
自分で書くには、以下のように書かなければなりません。

searched_number = 4
for i in [1,2,3,4,5,6]:
    if i == searched_number:
        print "find!"
        break

しかし、Pythonにはinと呼ばれるステートメントがあり、以下のように記載することができます。

searched_number = 4
if searched_number in list:
    print "find!"

関数について

返り値を複数返す。

実はPythonは返り値を複数返すことができます。

def return_function(a,b):
    return a,b

関数のオブジェクト化

関数はオブジェクトにすることができます。
条件分岐とかでどの関数を実行するかを選択する場合に主に利用しています。

def sum(a,b):
    return a + b

def diff(a,b):
    return a - b

func_obj = sum
print func_obj(1,2)