deeplearn.jsを使ってKaggleのタイタニックをやってみる

唐突ですが、Numeraiをナンピンし無限に損をし続けるにも限界があります。

そう、BTCがもう無いのです。そして心も折れました。

そんな沼底でGBYTEと共に耐え忍ぶ以外の方法を模索し、たどり着いた結論は損切りではなく「データサイエンティストになってNMRを貰おう」でした。

まぁ実際にはそこまで機械学習の沼は浅くないんでしょうが、前から興味があったので良い機会ということで手を出してみました。

ついでに裁量取引ではひどい目にばかり合うので、自動売買のBOTとか作ってみたいという思いもあり。

ということでここから暗号通貨は一切関係無しです。

deeplearn.js

まずは初めの一歩ということで、googleがらみの機械学習ライブラリdeeplearn.jsを少し触ってみました。

deeplearn.jsはブラウザからWebGLを介してGPUを使った機械学習が出来る優れもので、TensorFlowを触るより手軽なんじゃないかと考えて手を出しました。

しかし、機械学習初心者が初めに触るには少しハードルが高く、結局は機械学習の作法について理解するために、最も情報の多いTensorFlowを一から勉強する羽目になりました。

TensorGraphSessionあたりの概念はほぼ同じようなので無駄にはなりませんが、ライブラリでやれることもまだ少ないようなので、これから機械学習に手を出そうという人は素直にTensorFlow触ったほうがいいと思います。

Kaggle

今回は機械学習Hello worldこと、KaggleのTitanicを試してみます。

Kaggleは機械学習のアイデアのコンペをやっているサイトで、チュートリアル的なコンペがいくつかあり、そのうちの1つTitanicをやってみます。

Titanic: Machine Learning from Disaster | Kaggle

タイタニック号の乗員名簿からその生死を予測するというもので、必要なデータは以下にあるtrain.csvtest.csvです。

Titanic: Machine Learning from Disaster | Kaggle

データの前処理

データの前処理は非常に重要です。 むしろこちらが本番なのではないかという気すらします。

初めはこの辺りをあまり考えず、とりあえず数値になっていればいいだろうという考えでやってみましたが、全くうまくいきませんでした。

不要なデータの削除

以下は有用性が低い、もしくは扱いづらいデータです。

  • PassengerId
  • Name
  • Ticket
  • Cabin

使えないデータは列ごと削除します。 Cabinはすごく有用だと思われますが、欠損が多いので除外します。

ただ、マジな人々はTicketからCabinを割り出したり、Nameにある敬称からAgeを割り出したり色々使いどころはあるようです。

欠損値補完

以下はデータに欠損があります。

  • Age
  • Fare
  • Cabin
  • Embarked

平均値や中央値で補完するケースや、欠損のある列を除外するケース、欠損のある行を除外するケース、また機械学習で欠損している部分を推定するケースなど様々なようです。

今回は面倒なのでCabinは捨て、他は平均値で補完します。

標準化

以下はスケールの異なる数値データです。

  • Age
  • SibSp
  • Parch
  • Fare

スケールの異なるデータ間で数値の大小が極端に大きい場合、特定の項目の影響が大きくなりすぎるというようなことが起こるようで、それを避けるために各次元のスケールを合わせます。

一般的にはZスコアが利用されるそうです。

(x - 平均) / 標準偏差

データセットによっては、時系列データなど標準化すべきではないものもあります。

ダミー変数化

以下は数値データではありません。 Pclassは数値ですが、社会階級High、Middle、Lowを表しています。

  • Pclass
  • Sex
  • Embarked

こういったデータを扱う場合は、次元を拡張しmaleの場合[1, 0]、femaleの場合[0, 1]のように別の次元で表現します。

交差検証

過学習が起きていない事、汎化性能が高いことを検証するために、訓練データとテストデータを分け、訓練には訓練データを使い、精度の検証には訓練に使っていないテストデータを使います。

train.csvの前処理が終わったら、訓練データtrainXtrainYとテストデータtestXtestYに分割しておきます。 Xは前処理で作成した各種データの配列で、Yは答えとなるSurvivedの配列です。

deeplearn.jsのインストール

npmでインストールします。

npm install --save deeplearn

必要なものをライブラリからインポートしておきます。

import {
  Array1D,
  Array2D,
  NDArrayMathGPU,
  Scalar,
  Session,
  SGDOptimizer,
  InCPUMemoryShuffledInputProviderBuilder,
  CostReduction,
  Graph,
  Tensor,
  NDArray
} from 'deeplearn';

グラフの構築

Graphを使ってモデルを定義します。

訓練データやテストデータの入れ物になるxtgraph.placeholdershapeのみ決めて定義します。

訓練により最適化されていく変数となるw0b0w1b1graph.variableで初期値と共に定義します。

用意した変数に対してgraph.add(足し算)、graph.matmul(内積)などのメソッドを使って数式を組み、graph.relugraph.sigmoidなどの活性化関数を通して次の層へ出力します。

その他Graphクラスのメソッドについては、以下公式のAPI Referenceに記載されています。

Graph | deeplearn

今回実装したモデルはロジスティック回帰に隠れ層を追加した多層パーセプトロンで、隠れ層の数はいくつか増やしてみたりしたものの結果がいまいちだったので1層だけです。ニューロンの数の増減もあまり良い結果を生みませんでした。

なお、sigmoidに渡すときにreshapeしてあげないとshapeが合わない的なエラーが出て、結構ハマりました。

最後にgraph.meanSquaredCostで損失関数に二乗誤差を指定しています。

const graph: Graph = new Graph();
const x: Tensor = graph.placeholder("x", [12]);
const t: Tensor = graph.placeholder('t', []);

//入力層 - 隠れ層
const w0: Tensor = graph.variable("w0", Array2D.randNormal([12, 12]));
const b0: Tensor = graph.variable("b0", Scalar.randNormal([]));
const h0: Tensor = graph.relu(graph.add(graph.matmul(x, w0), b0));

//隠れ層 - 出力層
const w1: Tensor = graph.variable("w1", Array2D.randNormal([12, 1]));
const b1: Tensor = graph.variable("b1", Scalar.randNormal([]));
const y: Tensor = graph.sigmoid(graph.reshape(graph.add(graph.matmul(h0, w1), b1), []));

const cost: Tensor = graph.meanSquaredCost(y, t);

ちなみにとことんconstで定義している理由は謎です。公式に倣いました。

訓練

訓練(学習)はmath.scopeの中で行います。

訓練データをNDArrayとして構築する際にtrackで追跡することで、scopeの最後で自動的にクリーンアップされるようになります。

InCPUMemoryShuffledInputProviderBuilderで事前にデータをシャッフルします。これ結構重要らしいです。

NUM_BATCHESを増やして訓練を繰り返すほど学習が進んでいきますが、数を倍にしたところであまり結果に差異はありませんでした。

LEARNING_RATEを小さくすると訓練に時間がかかるようになるので、最適なNUM_BATCHESの数も変わりますが、色々試してもあまり顕著に良くなる組み合わせが見つかりませんでした。

オプティマイザはSGD(確率的勾配降下法)しかないようで、それ以外を使いたければ自力で実装するしかなさそうです。

ループの中でsession.trainを実行し、出力される値が徐々に小さくなっていけば訓練が進んでいるということになります。 全く動かない、安定しない、NaNになるというような場合は何かおかしいのでグラフの構築、もしくはデータの前処理から見直したほうが良いです。

const math: NDArrayMathGPU = new NDArrayMathGPU();
const session: Session = new Session(graph, math);

math.scope((keep, track) => {
  const xs: Array1D[] = trainX.map(x => track(Array1D.new(x)));
  const ys: Scalar[] = trainY.map(x => track(Scalar.new(x)));

  const shuffledInputProviderBuilder =
      new InCPUMemoryShuffledInputProviderBuilder([xs, ys]);
  const [xProvider, yProvider] =
      shuffledInputProviderBuilder.getInputProviders();

  const NUM_BATCHES = 500;
  const BATCH_SIZE = xs.length;
  const LEARNING_RATE = 1;
  const optimizer = new SGDOptimizer(LEARNING_RATE);
  for (let i = 0; i < NUM_BATCHES; i++) {
    const costValue = session.train(
        cost,
        [{tensor: x, data: xProvider}, {tensor: t, data: yProvider}],
        BATCH_SIZE, optimizer, CostReduction.MEAN
      );
    console.log("Average cost: " + costValue.get());
  }

推定

事前に作成したテストデータtestXを利用し、session.evalSurvivedを推定し、testYと比較して精度を確認します。

これは訓練のループの中で訓練ごとの精度を確認することも出来ますし、ループを抜けてから最後に実行することも可能です。

for(let i = 0; i < testX.length; i++) {
  const result: NDArray = session.eval(y, [{tensor: x, data: track(Array1D.new(testX[i]))}]);
  let r = result.getValues()[0] > 0.5 ? 1 : 0;
  console.log(r === testY[i]);
}

提出

精度に問題なければtest.csvに同じように前処理を実行し、上記と同じ手順でSurvivedを推定します。

PassengerIdと出力されたSurvivedcsvを作成し、以下ページでアップロードします。

https://www.kaggle.com/c/titanic/submit

結果、Scoreは0.78947でした。

f:id:tadajam:20170922032123p:plain

良いんだか悪いんだか良く分かりませんが、とりあえずベースラインは超えたので良しとします。

そして次は素直にTensorFlow使います。