deeplearn.jsを使ってKaggleのタイタニックをやってみる
唐突ですが、Numeraiをナンピンし無限に損をし続けるにも限界があります。
そう、BTCがもう無いのです。そして心も折れました。
そんな沼底でGBYTEと共に耐え忍ぶ以外の方法を模索し、たどり着いた結論は損切りではなく「データサイエンティストになってNMRを貰おう」でした。
まぁ実際にはそこまで機械学習の沼は浅くないんでしょうが、前から興味があったので良い機会ということで手を出してみました。
ついでに裁量取引ではひどい目にばかり合うので、自動売買のBOTとか作ってみたいという思いもあり。
ということでここから暗号通貨は一切関係無しです。
deeplearn.js
まずは初めの一歩ということで、googleがらみの機械学習ライブラリdeeplearn.js
を少し触ってみました。
deeplearn.js
はブラウザからWebGLを介してGPUを使った機械学習が出来る優れもので、TensorFlow
を触るより手軽なんじゃないかと考えて手を出しました。
しかし、機械学習初心者が初めに触るには少しハードルが高く、結局は機械学習の作法について理解するために、最も情報の多いTensorFlow
を一から勉強する羽目になりました。
Tensor
、Graph
、Session
あたりの概念はほぼ同じようなので無駄にはなりませんが、ライブラリでやれることもまだ少ないようなので、これから機械学習に手を出そうという人は素直にTensorFlow
触ったほうがいいと思います。
Kaggle
今回は機械学習のHello worldこと、KaggleのTitanicを試してみます。
Kaggleは機械学習のアイデアのコンペをやっているサイトで、チュートリアル的なコンペがいくつかあり、そのうちの1つTitanicをやってみます。
Titanic: Machine Learning from Disaster | Kaggle
タイタニック号の乗員名簿からその生死を予測するというもので、必要なデータは以下にあるtrain.csv
とtest.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
の前処理が終わったら、訓練データtrainX
、trainY
とテストデータtestX
、testY
に分割しておきます。
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
を使ってモデルを定義します。
訓練データやテストデータの入れ物になるx
とt
はgraph.placeholder
でshape
のみ決めて定義します。
訓練により最適化されていく変数となるw0
、b0
、w1
、b1
はgraph.variable
で初期値と共に定義します。
用意した変数に対してgraph.add
(足し算)、graph.matmul
(内積)などのメソッドを使って数式を組み、graph.relu
やgraph.sigmoid
などの活性化関数を通して次の層へ出力します。
その他Graph
クラスのメソッドについては、以下公式のAPI Referenceに記載されています。
今回実装したモデルはロジスティック回帰に隠れ層を追加した多層パーセプトロンで、隠れ層の数はいくつか増やしてみたりしたものの結果がいまいちだったので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.eval
でSurvived
を推定し、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
と出力されたSurvived
でcsvを作成し、以下ページでアップロードします。
https://www.kaggle.com/c/titanic/submit
結果、Scoreは0.78947でした。
良いんだか悪いんだか良く分かりませんが、とりあえずベースラインは超えたので良しとします。
そして次は素直にTensorFlow使います。