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

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

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

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

前回からあいてしまいましたが、引き続き
SensorBeeのturtorialをやってみます。

こちらは準備段階に2段階あり、elasticsearch,kibanaのインストールとtutrialの実施です。

準備

Elastisearch並びにkibanaのインストール

elasticsearch、kibanaは公式から取得するのが良いと思います。
公式ではversion2.2.0ですが、今回は最新の安定版2.3.2で実行してみます。

$ ./bin/elasticsearch
[2016-05-04 23:13:01,634][INFO ][node                     ] [Impulse] version[2.3.2], pid[41126], build[b9e4a6a/2016-04-21T16:03:47Z]
[2016-05-04 23:13:01,635][INFO ][node                     ] [Impulse] initializing ...

起動確認は以下のcurlコマンドで実施できます。
curlコマンドのレスポンスがかえって来れば問題ありません。nameなどは立ち上げるたびに変わるので・・

$ curl http://localhost:9200/
{
  "name" : "Aliyah Bishop",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.3.2",
    "build_hash" : "b9e4a6acad4008027e4038f6abed7f7dba346f94",
    "build_timestamp" : "2016-04-21T16:03:47Z",
    "build_snapshot" : false,
    "lucene_version" : "5.5.0"
  },
  "tagline" : "You Know, for Search"
}

kibanaの起動確認は以下の通り、こちらも公式サイトから取得しましょう。

$ ./bin/kibana
  log   [23:43:01.165] [info][status][plugin:sense] Status changed from uninitialized to green - Ready
  log   [23:43:01.171] [info][status][plugin:kibana] Status changed from uninitialized to green - Ready
  log   [23:43:01.193] [info][status][plugin:elasticsearch] Status changed from uninitialized to yellow - Waiting for Elasticsearch
  log   [23:43:01.207] [info][status][plugin:kbn_vislib_vis_types] Status changed from uninitialized to green - Ready
  log   [23:43:01.213] [info][status][plugin:markdown_vis] Status changed from uninitialized to green - Ready
  log   [23:43:01.224] [info][status][plugin:metric_vis] Status changed from uninitialized to green - Ready

tutorialの設定

$ go get github.com/sensorbee/tutorial/ml
$ cp -r $GOPATH/src/github.com/sensorbee/tutorial/ml/config/* /path/to/sbml/
$ cp -r $GOPATH/src/github.com/sensorbee/tutorial/ml/config/* ./sbml/
$ cd sbml/

fluentd

最後にfluentdです。(ここはLogstashではないのか・・・)
まず、初めにbundlerが入っていことを確認しましょう。versionがコンソールに表示されていなければ

$gem list | grep bundler

入っていなければ、以下のコマンドを実行しましょう。

$sudo gem install bundler

最後にbundle installを使って、fluentdを実行します。

$ bundle install --path vendor/bundle
$ bundle exec fluentd --version
fluentd 0.12.23

fluentdの起動確認をしましょう。以下のような表示がコンソールに出れば成功です。

$ bundle exec fluentd -c fluent.conf
2016-05-04 23:50:17 +0900 [info]: reading config file path="fluent.conf"
2016-05-04 23:50:17 +0900 [info]: starting fluentd-0.12.23
2016-05-04 23:50:17 +0900 [info]: gem 'fluentd' version '0.12.23'
2016-05-04 23:50:17 +0900 [info]: gem 'fluent-plugin-elasticsearch' version '1.4.0'
2016-05-04 23:50:17 +0900 [info]: adding match pattern="sensorbee.tweets" type="elasticsearch"
2016-05-04 23:50:17 +0900 [info]: adding source type="forward"
2016-05-04 23:50:17 +0900 [info]: using configuration file: <ROOT>
  <source>
    @type forward
    @id forward_input
  </source>
  <match sensorbee.tweets>
    @type elasticsearch
    host localhost
    port 9200
    include_tag_key true
    tag_key @log_name
    logstash_format true
    flush_interval 1s
  </match>
</ROOT>
2016-05-04 23:50:17 +0900 [info]: listening fluent socket on 0.0.0.0:24224
^C2016-05-04 23:50:34 +0900 [info]: shutting down fluentd
2016-05-04 23:50:34 +0900 [info]: shutting down input type="forward" plugin_id="forward_input"
2016-05-04 23:50:34 +0900 [info]: shutting down output type="elasticsearch" plugin_id="object:3fc04d87f990"
2016-05-04 23:50:34 +0900 [info]: process finished code=0

今回はtwitterを解析するので、api_keyの設定をします。
api_keyはtwitterの公式サイトより、取得することができます。

公式サイトに掲載されている以下の雛形にしたがって、api_key.yamlを作成しましょう。

$ go get gopkg.in/sensorbee/sensorbee.v0/...
$ build_sensorbee
sensorbee_main.go
$ ./sensorbee run -c sensorbee.yaml
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":{"dir":"uds"},"type":"fs"}},"topologies":{"twitter":{"bql_file":"twitter.bql"}}}
INFO[0000] Setting up the topology                       topology=twitter
INFO[0007] Starting the server on :15601

この時に立ち上がっていれば、kibana上で以下の画面を見ることができます。

まずは、kibana上で可視化したいelasticsearchのindexを指定します。
f:id:tereka:20160505002618p:plain

indexを指定すると以下の画面を見ることができます。

f:id:tereka:20160505002628p:plain

最後にDiscoveryをクリックするとデータが投入されている様子がわかります。

f:id:tereka:20160505002638p:plain

トラブルが発生している場合は・・・

sensorbee shell -t twitter

まずは、以下のコマンドを入力し、データが取得できているかを確認します。

twitter > SELECT RSTREAM * FROM public_tweets [RANGE 1 TUPLES];
twitter > SELECT RSTREAM * FROM labeled_tweets [RANGE 1 TUPLES];

データが取得できている場合は更に以下のコマンドを実行し、ストリーム生成処理が成功しているかを確認します。

CREATE SOURCE public_tweets TYPE twitter_public_stream
    WITH key_file = "api_key.yaml";

以下、暫くはBQLの紹介なのでスキップし、機械学習部分に行きます。

機械学習

まずは、既存にある機械学習のモデルを読み込みます。

twitter> LOAD STATE age_model TYPE jubaclassifier_arow
    OR CREATE IF NOT SAVED
    WITH label_field = "age", regularization_weight = 0.001;
twitter> LOAD STATE gender_model TYPE jubaclassifier_arow
    OR CREATE IF NOT SAVED
    WITH label_field = "gender", regularization_weight = 0.001;

評価はEVAL Statementを使うことで実施可能です。

twitter> EVAL jubaclassify("gender_model", {
    "text": {"i": 1, "wanna": 1, "eat":1, "sushi":1},
    "description": {"i": 1, "need": 1, "sushi": 1}
});

maleかfemaleのどちらかであることを評価するためにはjuba_classified_labelを使用します。

twitter> EVAL juba_classified_label({
    "male":0.021088751032948494,"female":-0.020287269726395607});

機械学習のトレーニングに躓いているので、こちらは余裕があればやることにします。

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)