synaptic.js

アケオメコトヨロ

機械学習の事を少し勉強するかってんで、図書館へ行ったんだけど、トランプなんて 時代の寵児の本は有るのに、関係本は皆無。田舎だからITは遅れているんかいな。

折角なので寵児の本と、機械学習に関係あるか微妙な、図解雑学『多変量解析』なんて本と、 幻の料亭、日本橋「百川」なんてのを借りてきた。我ながら、脈絡が無い選択。

多変量解析って名前は知ってたけど、色々な突っ込み方法が有るのね。 重回帰分析、主成分分析、因子分析、判別分析。

分析対象が数値で表れるなら、これらを 使えばいいんだけど、アンケートみたいなのは、数値でない回答も多い。 そういう場合は、無理して数量化し、上記分析に持ち込むとか。 この数量化にも、数量化1類と数量化2類とあるとか。数学I、数学IIみたいなものか。 1類は、説明変量を数値に置き換えるそうな。2類は目的変量も無理して数値にして、 強引に分析する方法とか。苦労するのね。

説明の舞台は、ファミレスを新しい街に展開する事になった企画部の人の苦悩で始まる。 今までの実績報告書を読み解いて、新しい店の売り上げ予測を設計するという占い師 もどき。

店舗面積、従業員数、座席数、駐車場の収容台数、駅からの距離等のデータと売り上げ 実績表があり、これを元に占いをする。

その数学的手口が色々解説されてた。統計勉強しとけって事ですな。 これと機械学習が、どう繋がるのかな。思いを馳せてみよう。

ちゃんとやるなら、これを見ておけ

ニューラルネットワークと深層学習

これからでも遅くない、こっそり学ぶディープラーニング

神経細胞を分解する

synapticはJavascriptで深層学習をやるためのパッケージ。去年の暮にやった。

場合によっては、学習を拒否して不貞腐れた回答しか返さない事があった。試行錯誤してたら 学習回数が足りない事に気が付いた。30回の学習では不足な場合が有るんだ。大体60回ぐらい の試行で、知恵を付けてくれる模様。

だったら、安全を見て100回すか。ただ、無暗に回すと場合によっては不都合を生じる そうだ。訓練データで良い点を取れるように学習してしまい、本番では対応出来なくなる って問題、いわゆるオーバーフィッティングって言う、やっかいなのが首をもたげてくる。

そんな事もあろうかと、あるエラーレートを達成出来たら、それ以上の訓練を止めるような 機構が組み込まれている。

// データを学習する
const trainer = new Trainer(bld_network);
trainer.train(data, {
  rate: 0.2, iterations: 1000, error: 0.6,
  shuffle: true, log: 1,
  cost: Trainer.cost.CROSS_ENTROPY
});

最大1000回訓練するけど、その間にエラー率が0.6を下回ったら訓練終了、とかの設定を しておけばよい。

試行する度に訓練回数が異なるけど、それは何故?って疑問が前回有った。怪しい設定の shuffleをfalseにしておいてから、実行しても、訓練回数が異なる。全く同一の訓練 データを与えているんだから、同じ挙動(訓練回数)になって欲しい。

この謎を解くべく、debugモードで中へ踏み入ってみる。

Node.js v6.9.2 Documentation Debugger

> inputLayer
{ size: 3,
  list:
   [ { ID: 0,
       label: null,
       connections: [Object],
       error: [Object],
       trace: [Object],
       state: 0,
       old: 0,
       activation: 0,
       selfconnection: [Object],
       squash: [Function: val],
       neighboors: {},
       bias: -0.0699729337386613 },
     { ID: 1,
       label: null,
       connections: [Object],
       error: [Object],
       trace: [Object],
       state: 0,
       old: 0,
       activation: 0,
       selfconnection: [Object],
       squash: [Function: val],
       neighboors: {},
       bias: 0.08834052530961958 },
        :

入力のニューロンの設定が終わった所で、中を覗いてみた。bias値が、ニューロンごとに 異なっている。これって、ひょっとしてランダムな値を割り当てていないかい。 折角、ソースが有るんだから、ひも解いてみるか。

function Neuron() {
  this.ID = Neuron.uid();
   :
  this.squash = Neuron.squash.LOGISTIC;
  this.neighboors = {};
  this.bias = Math.random() * .2 - .1;
}

これを見ると、やっぱり乱数を使って、ニューロンに揺らぎを与えているな。 そして、squash(押し潰し)の関数にLOGISTICと呼ばれるのを使ってる。 詳しい説明はここの一番下の リンクを参照。

このLOGISTICをどんな風に実現してるか、ソースを見ると

      var derivative = getVar(this, 'derivative');
      switch (this.squash) {
        case Neuron.squash.LOGISTIC:
          buildSentence(activation, ' = (1 / (1 + Math.exp(-', state, ')))',
            store_activation);
          buildSentence(derivative, ' = ', activation, ' * (1 - ',
            activation, ')', store_activation);
          break;
        case Neuron.squash.TANH:
         :

どうも、こんなの見ちゃうと、文字列をevalしてるのかなあ、なんて勘ぐってしまうぞ。 まあ、動的にコードを組み立てたいって気持ちは分かりますよ。

続いて、データを学習する部分を見ておく。

< iterations 1 error 2.9121842004531744 rate 0.2
break in /home/sakae/node_modules/synaptic/src/trainer.js:114
 112           console.log('iterations', iterations, 'error', error, 'rate', currentRate);
 113         };
>114         if (options.shuffle)
 115           shuffle(set);
 116       }

丁度、ログ出力が有ったあたり。trainerって、あの犬の調教師みたいだな。ひたすら訓練を かさねてるんだろうね。このあたりを精査すれば、どうやって学習してるか分かるな。

次は、学習が終了して、いよいよテストの所をみる。

s
break in lean-data.js:52
 50 function t(ymdh, high, low, pls) {
 51   const labels = ["am", "pm"];
>52   let n = bld_network.activate(setInput(high, low, pls));
 53   let result = labels[argmax(n)];
 54   let exp = ymdh % 100 < 12 ? "AM" : "PM";
s
break in [unnamed]:278
 276  F[273] = 0;
 277  var activate = function(input){
>278 F[1] = input[0];
 279  F[2] = input[1];
 280  F[3] = input[2];

debuggerは、通常、ソースファイル名と行数を冒頭で報告してくるんだけど、今回は、 名無しファイルって言ってる。学習成果が、式の羅列に変換されて、信号が通る道が 決定したって事だな。後は、その道に従って、粛々と計算を進めるだけだ。 ステップ実行で、先を追うと

break in [unnamed]:284
 282  F[6] = F[7];
 283  F[6] += F[1] * F[8];
>284  F[6] += F[2] * F[9];
 285  F[6] += F[3] * F[10];
 286  F[4] = (1 / (1 + Math.exp(-F[6])));

F(7)に入っているバイアス値をF(6)に設定。続いて、入力信号に重みを掛け算し、それをF(6)に足しこみする。 そして、それを圧縮する。結果は、次段の信号入力になるとな。

break in [unnamed]:516
 514  output[0] = F[230];
 515  output[1] = F[252];
>516  return output;
 517  };
 518  var propagate = function(rate, target){

break in [unnamed]:517
 515  output[1] = F[252];
 516  return output;
>517  };
 518  var propagate = function(rate, target){
 519 F[0] = rate;

break in lean-data.js:53
 51   const labels = ["am", "pm"];
 52   let n = bld_network.activate(setInput(high, low, pls));
>53   let result = labels[argmax(n)];
 54   let exp = ymdh % 100 < 12 ? "AM" : "PM";
 55

これが、ユーザースクリプトに復帰した所。動的に作られた行の278行目から519行まで 実行してるんで、判定に要した行数は、241行分。やってる事は単純だけど、前段階で、 経路の強化と重みの決定に苦労してるんだな。

何となく、分かったような気分になるな。もっとわかりたければ、network.jsに端を 発する、hardcodeって言う文字列の組み立て方を見れば良いのかな。

隠れ層を5と小さくして、bld_networkの値をダンプしてみた。

break in lean-data.js:52
 50 function t(ymdh, high, low, pls) {
 51   const labels = ["am", "pm"];
>52   let n = bld_network.activate(setInput(high, low, pls));
 53   let result = labels[argmax(n)];
 54   let exp = ymdh % 100 < 12 ? "AM" : "PM";
repl
Press Ctrl + C to leave debug repl
> bld_network
{ layers:
   { input: { size: 3, list: [Object], label: null, connectedTo: [Object] },
     hidden: [ [Object] ],
     output: { size: 2, list: [Object], label: null, connectedTo: [] } },
  optimized:
   { memory:
      { '0': 0.2,
        '1': 0.625,
        '2': 0.79,
        '3': 0.58,
        '4': 0.9995388744466385,
        '5': 7.666214412804138,
        '6': 7.6813789701187964,
        '7': 3.422385368114699,
        '8': 2.0037310890503255,
        '9': 2.246166553856887,
        '10': 2.124427363548843,
        '11': 0.00046091291658556695,
          :
        '96': 0.9995186971198726,
        '97': 0 },
     activate: [Function: val],
     propagate: [Function: val],
     ownership: [Function: val],
     data:
      { variables: [Object],
        activate: [Object],
        propagate: [Object],
        trace: [Object],
        inputs: [Object],
        outputs: [Object],
        check_activation: [Function: val],
        check_propagation: [Function: val] },
     reset: [Function: val] },
  activate: [Function: val],
  propagate: [Function: val] }

evalを使って、組み立てたコードを実行してるかと思ったら、evalがスクリプト中に 見つからない。別な評価方法が無ければおかしいと思って調べたら、 Functionによるevalの代替なんてのに行き当たった。Javascript面白い。

学習済データ

以上の解析で、どんな事をやってるか分かった(つもり)。世のフレームワークを見て いると、学習に時間がかかるので、学習済のデータが提供されている事がある。後は 試験データを流すだけってお手軽さ。

今試してるやつもそんな事が出来ないだろうか? 上で見たhardcodeってのが、どうやら 学習データに相当するっぽい。これが何処で使われるかと言うと、networl.jsで ハンドリングされてる。

    173     hardcode +=
    174       "return {\nmemory: F,\nactivate: activate,\npropagate: propagate,\
nownership: ownership\n};";
    175     hardcode = hardcode.split(";").join(";\n");
    176
    177     var constructor = new Function(hardcode);
    178
    179     var network = constructor();

例をbmiに変更して、隠れ層を小さくしたやつで、調べてみるかな。

sakae@debian:~/js/bmi$ node debug learn-data.js
< Debugger listening on [::]:5858
connecting to 127.0.0.1:5858 ... ok
break in learn-data.js:2
  1 // synaptic.jsの取り込み
> 2 const synaptic = require('synaptic');
  3 const Layer = synaptic.Layer;
  4 const Network = synaptic.Network;
sb('network.js', 177)
Warning: script 'network.js' was not loaded yet.
  1 // synaptic.jsの取り込み
> 2 const synaptic = require('synaptic');
  3 const Layer = synaptic.Layer;
  4 const Network = synaptic.Network;
  5 const Trainer = synaptic.Trainer;
  6 // 各レイヤーを生成
  7 const inputLayer = new Layer(2);
c
break in /home/sakae/node_modules/synaptic/src/network.js:177
 175     hardcode = hardcode.split(";").join(";\n");
 176
>177     var constructor = new Function(hardcode);
 178
 179     var network = constructor();
bt
#0 Network.optimize network.js:177:23
#1 Network.activate network.js:38:14
#2 Trainer._trainSet trainer.js:147:33
#3 Trainer.train trainer.js:100:23
#4 learn-data.js:34:9

ファイル名と行番号を指定してBPを貼り、contineu。止まった所で、呼び出し履歴を確認。 ここまでは、普通のdebuggerだな。後は、replに入って、hardcodeを表示すればいいな。

repl
Press Ctrl + C to leave debug repl
> hardcode
'var F = Float64Array ? new Float64Array(59) : [];\n F[0] = 0;\n F[1] = 0;\n F[2] = ... (length: 3248)'

長い文字列だと、気を利かせて、途中を省略してしまうんか。面倒だな。どうしよう。

> console.log(hardcode)
< var F = Float64Array ? new Float64Array(59) : [];
<  F[0] = 0;
<  F[1] = 0;
<  F[2] = 0;
<  F[3] = 0;
<  F[4] = 0;
<  F[5] = 0;
<  F[6] = 0.05298811777791346;
<  F[7] = 0.08281325334777528;
<  F[8] = 0.012445847099815846;
   :
<  F[58] = 0;
<  var activate = function(input){
< F[1] = input[0];
<  F[2] = input[1];
<  F[4] = F[5];
<  F[5] = F[6];
<  F[5] += F[1] * F[7];
<  F[5] += F[2] * F[8];
<  F[3] = (1 / (1 + Math.exp(-F[5])));
<  F[9] = F[3] * (1 - F[3]);
   :
<  F[57] = F[29];
<  var output = [];
<  output[0] = F[41];
<  output[1] = F[50];
<  return output;
<  };
<  var propagate = function(rate, target){
< F[0] = rate;
<  F[49] = target[0];
<  F[58] = target[1];
   :
<  F[7] += F[0] * (F[12] * F[10]);
<  F[8] += F[0] * (F[12] * F[11]);
<  F[6] += F[0] * F[12];
<   };
< var ownership = function(memoryBuffer){
< F = memoryBuffer;
< this.memory = F;
< };
< return {
< memory: F,
< activate: activate,
< propagate: propagate,
< ownership: ownership
< };

学習された値が初期値としてメモリー(配列)に登録されてる。activiateで、試験データを 評価するんだな。propagateって関数も登録されてる。この英語、伝搬って意味だから、 ニューロンの重みとかを更新する時(学習時)に使われのだろう。

やはり、肝はnetworkに有りだな。よーく分かったよ。

Lisp/scheme/haskell 深層学習

Javascript以外の何かで、機械学習をやってる例が無いか 探してみた。例によって、それ以外も混じっているけど、 一期一会って事で。。。

Tutorial:Lispで人工知能

「Common Lisp と 人工知能プログラミング」を出版しました

Common LispによるAI Programming入門 (2013)

DeepLearning(1): まずは順伝播(上)

深層学習入門

Tensorflowを1ヶ月触ったので超分かりやすく解説

ISLispによるGPU行列計算 -深層学習理解へ向けて-

みなさん、Haskellで3層ニューラルネットによる誤差逆伝搬を実装しましょうチュートリアル第1回

実践 Haskell 】作業領域別 tutorialサイトまとめ ~ ファイル入出力、グラフ描画、線形回帰、統計解析・多変量解析、機械学習、ニューラル・ネット、ディープ・ラーニング、自然言語解析、DB操作、Webアプリケーション・フレームワーク