【PHP FANN】PHPでもニューラルネットワークが扱えるらしいのでさわってみた


PHPのモジュールのひとつに[FANN(Fast Artificial Neural Network)](http://php.net/manual/ja/book.fann.php)というものがあります。
データ解析系だとまずはRでやろうとなるのですが、Webベースで簡単な機械学習ができそうかもと思って使ってみたメモです。
まだドキュメントちょっとずつ読みながら試している途中なので、随時更新していくと思います。

#FANNとは
C言語で書かれたオープンソースのニューラルネットワークライブラリで、特にPHP専用のモジュールというわけではありません。
[Fast Artificial Neural Network Library (FANN)](http://leenissen.dk/fann/wp/)
特徴として

 - 誤差逆伝播法(BackProp)による学習を主にサポート
 - 動作がかなり高速
 - 最短3ステップで簡単に機械学習(リソース作成、学習、実行)
 - 英語ドキュメントは豊富である

等があります。

日本語ページは極端に少ないですが、概要は以下のページが参考になります。
[FANN - 日本大学](http://www.eml.ele.cst.nihon-u.ac.jp/~momma/wiki/wiki.cgi/FANN.html)

#PHPにおけるFANN
上記のFANNライブラリをPHPから扱えるようにしたものがあります。
冒頭リンクが公式ドキュメントですが、和訳されていないうえに例もほとんどなく、機能的にも全てが使えるわけではなさそうです。
日本語で書かれた先例もほぼ無いですが、こちらが面白かったです。
[PHP FANN (Fast Artificial Neural Network) を使って儲けてみる](http://slywalker.hateblo.jp/entry/2015/02/24/123214)



#インストール
デフォルトのままでは使えません。まずFANN本家のライブラリをインストールします。
Redhat系: `$ sudo yum install fann-devel`
Ubuntu系: `$ sudo apt-get install libfann-dev`
Mac OS: `$ brew install homebrew/science/fann`
Windowsとかbrewとかについては割愛。

次にPECLでFANNをインストールします。
(要するにラッパーなんですかねこれ)
`$ sudo pecl install fann`


パッケージ詳細はこちら
[PECL :: Package :: fann - PHP](https://pecl.php.net/package/fann)

確認として、`<?php phpinfo(); ?>`を一行だけ書いたphpファイルを実行してみましょう。
下記のようにfannという項目が出ていればOKです。
![Screen Shot 2016-05-19 at 11.35.22.png](https://qiita-image-store.s3.amazonaws.com/0/116011/c313cb31-a6f9-de30-6cf6-16ae0a78a723.png "Screen Shot 2016-05-19 at 11.35.22.png")




#使う前にANNの基本メモ
以下、ニューラルネットワークをANN(Artificial Neural Network)と略します。
ANNに関しては豊富な知識がネット上にありふれていますので、リンクまとめと簡単なメモだけ。

##概要
ANNは機械学習といわれるデータ解析手法のひとつで、脳の神経細胞間のネットワークをもとにした数学モデルです。
参考: [ニューラルネットワーク（NN）](http://www.geocities.co.jp/SiliconValley-Cupertino/3384/nn/NN.html)

簡単に言うとANNはいわゆる「関数」であり、この「関数」にデータセットを放り込むことで解を得ることができます。
基本的な流れは2段階で、学習と実行があります。
「学習」が、既存のデータを用いた「関数の(自動)設計」で、
「実行」が、その関数に実際にデータを入力して解出力を得るものになります。

##学習
脳の細胞間接続の具合ははじめから設計されているのではなく、外部環境(=入力)によってさまざまに変動しながら最適化されていきます。
ANNもこれと同じで、ネットワークの設計は与えられた入力と答えをもとにして最適になるように構築されていきます。
参考: [教師あり学習 | 東京大学グローバル消費インテリジェンス寄付講座](http://gci.t.u-tokyo.ac.jp/tutorial/supervised/)

答えを元にして自動でネットワークを調整することを「教師あり学習」といい、調整をする手法のひとつとして「誤差逆伝播法(バックプロパゲーション法)」が知られています。
[逆伝播の仕組み](http://nnadl-ja.github.io/nnadl_site_ja/chap2.html)

流れとしては、

1. とりあえずランダムにネットワークをつくる(厳密にはグラフの重みをランダムに設定する)
2. 実際に学習させたい事象について入力と出力のデータセットを用意する
3. データセットの入力をいれてとりあえず解の推定値を得る
4. その解とデータセットの出力(実際の解)を比較して誤差を算出し、その誤差が小さくなるようにネットワークを調整する

のようになります。
段階4において、出力に一番近いネットワークの結合から入力に向かって逆方向に調整されていくので「誤差逆伝播」と呼ばれています。

##実行
実際のデータを入力して解を得ます。
多次元の非線形関数で、ノイズに強い特徴があります。

##問題点
誤差逆伝播法によるANNには問題もあります。

###局所解(ローカルミニマム)
学習を進めていくと基本的にはどんどん誤差が減少していきます。
しかし誤差逆伝播法では誤差がだんだん小さくなるように設計される特性上、局所的に誤差が大きくなってしまう部分ではそれ以上学習を進めることができなくなる場合があります。
[正確さへのこだわりを捨てる――「やわらかい情報処理」再び（3）：ITpro](http://itpro.nikkeibp.co.jp/members/NBY/techsquare/20050602/162010/)
[[Think IT] 第2回：ニューラルネットワークの構造を知る！ (3/3)](https://thinkit.co.jp/article/30/2/3.html)

###過学習
多く学習をこなすと、誤差は最小になって決まったパターンの問題に対しては高精度で解を求めることができます。
その反面、少しでも違ったパターンが入力されるとまったく見当違いの解を出してしまいます。
これを過学習といい、入力のノイズに対して弱い、柔軟性のないシステムになります。
歳をとった方は頭がカタい人が多いとよく言われることがありますがそれと同じです。
[誤差逆伝播法の問題点 | 村上・泉田研究室 ニューラルネットワーク - 画像処理・理解研究室](http://ipr20.cs.ehime-u.ac.jp/column/neural/chapter6.html#4th)


#かんたんな関数の使い方
さてさてやっとPHPで使ってみます。
基本的には、冒頭で紹介したこちらの記事を参考にさせていただいております。先人に感謝。
[PHP FANN (Fast Artificial Neural Network) を使って儲けてみる](http://slywalker.hateblo.jp/entry/2015/02/24/123214)


##リソース作成
いくつか関数はありますが、例として配列からANNのベースとなるリソースを作成します。

```php
$layers = [3, 5, 1];
$ann = fann_create_standard_array(count($layers), $layers);
```

`$layers`は配列で、ここでは入力層が3ノード、中間層が5ノード、出力層が1ノードのANNを示しています。
これを引数として`fann_create_standard_array`を実行すると、FANNリソース`$ann`が返ってきます。
リソース作成後においては入力層と各中間層にひとつずつバイアスニューロンが自動で追加されるため、この例では入力層が4ノード、中間層が6ノードになります。

##リソース設定
次に設定ですが、デフォルトで適当な値がある程度設定されているのでほとんどさわらなくてもできる感じにはなっています。

```php
fann_set_activation_function_hidden($ann, FANN_ELLIOT_SYMMETRIC);
fann_set_activation_function_output($ann, FANN_ELLIOT_SYMMETRIC);
```

ただし、活性化関数は学習に大きく影響するので調整したほうがいいかもしれません。
これは上記の命令で、第一引数にリソースをとって第二引数に種類を指定することでセットできます。
主に第二引数には以下が指定できます。
`FANN_LINEAR` : y=xで表される直線の一次関数
`FANN_THRESHOLD` : xが0以上だとy=0,未満だとy=1.いわゆるステップ関数
`FANN_SIGMOID` : 出力が0~1のシグモイド関数 よく使われます
`FANN_SIGMOID_SYMMETRIC` : 出力が-1~1のシグモイド関数 いわゆるハイパボリックタンジェント
`FANN_ELLIOT` : 出力が0~1でシグモイドより収束の早い関数
`FANN_ELLIOT_SYMMETRIC` : 上のElliot関数の出力を-1~1にしたもの
`FANN_SIN` : 出力が-1~1の通常のサイン関数
`FANN_SIN_SYMMETRIC` : 出力が0~1に収まるよう圧縮されたサイン関数
これら活性化関数の詳細な式とそのほかの式は[こちら](http://libfann.github.io/fann/docs/files/fann_data-h.html#fann_activationfunc_enum)から。

このあたりはまだ触りきれていない感じです。


##データの学習

```php
$data = array(
	["input" => [101.81, 102.11], "output" => [1]],
	["input" => [101.55, 101.92], "output" => [0]],
	["input" => [100.98, 101.03], "output" => [1]],
	...
	..
);

$data = .. // 正規化処理

fann_reset_MSE($ann);
foreach($data as $learn){
	fann_train($ann, $learn["input"], $learn["input"]);
}

$mse = fann_get_MSE($this->ann);
```

学習させたいデータの配列をあらかじめ作っておきます。
気をつけたいのはデータの数値の範囲で、基本的には入力は0~1の範囲(活性化関数によっては-1~1)にします。
教師データ(出力)は0,1(これも関数によっては-1,0,1)というふうに離散値で入れることが望ましいようです。(要出典)
この例では為替のドル円の数値をいれてみましたが、例えば1ドル100円〜120円の幅を-1~1にスケールしてやるなどの「正規化」が必要です。
[ニューラルネットワークでデータを正規化するには? - 科学 | 【OKWAVE】](http://okwave.jp/qa/q4244732.html)

あとはデータを繰り返し`fann_train`にかけます。
終わったらMSE(Mean Squared Error: 二乗誤差)を取得して学習の度合いを確認しましょう。



##テスト実行
```php
$test = [100.08, 100.57];
// $test も正規化する必要がある
$result = fann_run($ann, $test);
```

学習で入力したのと同じ要素の数を持つテストデータをひとつだけ入れてあげると、配列で結果が返ってきます。
結果も正規化されたあとの範囲なので、実際の値に戻してやる必要があります。(ここでは割愛、すみません)



#ラッパークラスを作ってみた
ANNは状態遷移をもつのに手続き型命令なので使いにくい…
ということで最低限の命令だけラップして、簡単な統計処理を含めたクラスを作ってみました。
サンプルもつけています。
[GitHub | ukkz/php-fannWrapper](https://github.com/ukkz/php-fannWrapper)

基本的なメソッドは以下に。

```php
// ANN層構造指定
$layers = [5, 8, 3];

// 正規化の範囲指定
$range = array("max" => 1.0, "min" => -1.0);

// リソース作成
$ann = new fannWrapper($layers);

// データセットを用意して統計情報を得ておく
$data_raw = [ ... ];
$stats = fannWrapper::stats($data_raw);

// データの正規化
$data = fannWrapper::scaling($data_raw, $range["max"], $range["min"]);

foreach($data as ..){
	// 入力と出力を適当に処理して$inputと$outputに
	// 学習実行
	$ann->train($input, $output);
}

// テストデータを実行し、もとのデータの範囲にスケールする
$test = [ ... ];
$result = fannWrapper::scaling($ann->run($test_n), $stats["max"], $stats["min"], $range["max"], $range["min"]);

// HTML Canvasで視覚的に表示できるようにノードの配置情報等を出力
$canvasX = 800; $canvasY = 450; $padding = 50;
$neuron_position = json_encode($ann->visualize($canvasX, $canvasY, $padding));
$neuron_connection = json_encode($ann->getConnection());

```


#実用に耐えるか使ってみる
実際のデータを入れて使ってみます。
今回は為替変動をなんとなく学習させてみました。
データは[こちら](http://fx.sauder.ubc.ca/data.html)から日足のドル円を2年分取得し、適当な期間を取り出して使っています。
ANNでの為替予測は古くより行われているだけに先例は豊富です。
こちらなどは非常に精度が高くて驚きます。
[コンピュータで為替変動を予測する](http://anopara.net/2013/03/14/%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF%E3%81%A7%E7%82%BA%E6%9B%BF%E5%A4%89%E5%8B%95%E3%82%92%E4%BA%88%E6%B8%AC%E3%81%99%E3%82%8B/)

さて、PHP FANNによるコードは以下のようになりました。
自作ラッパー関数使用のため多少分かりにくくなっている点はすみません。

```test.php
<?php
require_once("fannWrapper.php");


// ##############################
// ### 学習範囲
// 0から790日目までで任意の範囲
	$start = 250;
	$stop = 350;
// ### ニューラルネットワークの層構造の指定
// 入力層は7、出力層は1
	$layers = [5, 10, 3, 1];
// ### 正規化の範囲
// 活性化関数に***_SYMMETRICを指定する場合は1から-1でそれ以外は1から0
	$range = array("max" => 1.0, "min" => -1.0);
// ### テスト配列
// 直近5日間のドル円レート
	$test = [100.29, 101.40, 101.11, 100.97, 101.03];
// ##############################


// FANNラッパークラス
$ann = new fannWrapper($layers);

// データを読み込み
$json = file_get_contents("usdjpy.json");
$data = json_decode($json, true);

// 与えられた範囲を抜き出して数値のみの配列をつくる
$usdjpy = [];
for ($i=$start; $i < $stop; $i++) {
	if (!isset($data[$i])) break;
	$usdjpy[] = (float)$data[$i]["rate"];
}

// 配列の統計情報を連想配列で返す
$stats = fannWrapper::stats($usdjpy);

// 正規化
$usdjpy = fannWrapper::scaling($usdjpy, $range["max"], $range["min"]);

foreach ($usdjpy as $k => $v) {
	if (!isset($usdjpy[$k+5])) break;
	// 入力配列
	$input = [$usdjpy[$k], $usdjpy[$k+1], $usdjpy[$k+2], $usdjpy[$k+3], $usdjpy[$k+4]];
	// 出力配列
	$output = [$usdjpy[$k+5]];
	// 学習実行
	$ann->train($input, $output);
}

// 学習結果を保存(オプション)
$ann->save("usdjpy.net");

// 予測テスト実行
// テストデータは実世界のものなので、教育したデータにあわせて正規化して入力
$test_n = [];
for ($i=0; $i < count($test); $i++) {
	$test_n[$i] = fannWrapper::scaling([$test[$i]], $range["max"], $range["min"], $stats["max"], $stats["min"]);
}
$result = fannWrapper::scaling($ann->run($test_n), $stats["max"], $stats["min"], $range["max"], $range["min"]);

// 表示
echo $result[0];
?>
```


#結果
結果は学習によって得られたデータをHTML Canvasと、jQuery flotr2を使用して図におこしました。

##ネットワーク図
![Screen Shot 2016-05-19 at 12.34.59.png](https://qiita-image-store.s3.amazonaws.com/0/116011/8fdb3e8d-1c6e-6227-d509-319b01d11e87.png "Screen Shot 2016-05-19 at 12.34.59.png")

左のピンク色ノードが入力、右の赤ノードが出力です。
5日分のデータを入力し、1日分を出力しています。
中間層は黒ノードで、緑と青のノードはバイアスを示しています。
結合の重さによって黒色グラフの濃さが変化しています。

##為替変動と二乗誤差推移
![Screen Shot 2016-05-19 at 12.35.15.png](https://qiita-image-store.s3.amazonaws.com/0/116011/ef0cecab-43f7-b1ef-eb33-da00cc0127f3.png "Screen Shot 2016-05-19 at 12.35.15.png")

黄緑色がドル円の為替相場データ、水色が二乗誤差の推移です。
基本的に誤差は減少していくのですが、為替が大きく変動したときに一時的に誤差が増大している様子がわかります。

##推定レートと統計
![Screen Shot 2016-05-19 at 12.35.30.png](https://qiita-image-store.s3.amazonaws.com/0/116011/6921dccc-bdfd-f80e-c150-196b284ce13c.png "Screen Shot 2016-05-19 at 12.35.30.png")

上の表が最終的な二乗誤差の値と推定レートです。
推定値はここではおよそ101.93円ですが、実際には101.827円でした。トレーダーにとっては大きな誤差ではありますが、前日に比べて上昇傾向にあるだろうという推測には使えそうです。

下の表は学習に利用したデータに関する統計です。
自作クラスにひととおりの処理を入れてみましたが思いのほか便利でした。


#まとめ
- PHPだけでも一応は機械学習ができる！
- ローカルでやるだけならやっぱりRとかPythonがいいかとおもいます。