TypeScript入門 – 機械学習の実装 2 Logistic Regression

TypeScript_Logo
前回はTypeScript入門ということで、TypeScriptで Denoising Autoencoders という種類のニューラルネットワークを作ったのと、AngularJSやAngular Materialの使い方を少し学ぶことができました。

このDenoising Autoencoderを構成要素として何層も積み重ねるとStacked Denoising Autoencoderとなり、Deep Learning(深層学習)とも呼ばれます。Denoising Autoencoderを実装してあれば、残りは出力層での教師有り学習で用いるロジスティック回帰やソフトマックス関数など小さな部品を作るだけです。実はそれとは別に、前回の記事の後に小規模なConvolutional Neural Network(CNN)もTypeScriptで書いてみたのですが、MNISTの学習すら一向に終わらないので少し実装を見直しているところです。漢なら手書きasm.jsに挑むべきって言われたんですけど、なかなかロマンを感じますね。

とりあえず今回は追加部品として作ったロジスティック回帰(Logistic Regression)を動かして正しく学習できているか見てみます。

ソースコードは今回もGitHubにアップしていますので興味があれば。

ロジスティック回帰なので分類タスクとなります。まずは簡単な二値分類から。
[javascript]
// 2クラスのデータを作成
var d: number = 2, N: number = 100,
x1: number[][] = createSample(d, N),
x2: number[][] = createSample(d, N),
x3: number[][] = createSample(d, N),
x4: number[][] = createSample(d, N),
y1: number[][] = createLabel(d, 1, N),
y2: number[][] = createLabel(d, 2, N),
y3: number[][] = createLabel(d, 1, N),
y4: number[][] = createLabel(d, 2, N);

for(var i=0; i 0) {
return c * Math.sin(Math.PI * 2 * b) * v + m;
} else {
return c * Math.cos(Math.PI * 2 * b) * v + m;
}
}
[/javascript]
* 出力結果

Loss: 7.484437784160068
Loss: 7.4823515706712955
Loss: 7.480910710736696
Loss: 7.47957441184471
Loss: 7.478276584000806
... 省略
Loss: 0.11801311268755298
Loss: 0.11730614586557697
Loss: 0.11661028260673367
Loss: 0.11592528080159488
Loss: 0.11525090507107187
[ 200, 2 ] [ 200, 2 ] [ 200, 2 ] [ 200, 2 ]  ## 2次元 2クラス 学習データ数 200, テストデータ数 200
accuracy: 0.995  ## 99.5%の正答率

データ作成時にランダム要素が入っているので実行の度に結果は多少変わりますが、予測結果を見るにきちんと分類できていますし、クロスエントロピー損失も徐々に減っているのを確認できました。次は多クラス分類、iris(アヤメ)とdigits(手書き数字)データセットを使います。どちらもscikit-learn(sklearn.datasets)に実装されているので、TypeScriptから読み込めるオブジェクトに変換しておきました。

Large53digits_data

[javascript]
// irisデータ (学習:テスト 4:1 で分割)
var x1 = iris.data,
y1 = iris.target;
trainSize: number = Math.ceil(x1.length*0.8);
testSize: number = x1.length – trainSize;
trainData: number[][] = x1.slice(0, trainSize);
trainLabel: number[] = y1.slice(0, trainSize);
testData: number[][] = x1.slice(trainSize, trainSize + testSize);
testLabel: number[] = y1.slice(trainSize, trainSize + testSize);

var x: ml.Matrix = new ml.Matrix(trainData);
var y: ml.Matrix = new ml.Matrix(ml.Preprocessing.binalizeLabel(trainLabel));

var clf: ml.LogisticRegression = new ml.LogisticRegression(x, y);

clf.fit(0.1, 100, 0.00, false);

var tx: ml.Matrix = new ml.Matrix(testData);
var ty: ml.Matrix = new ml.Matrix(ml.Preprocessing.binalizeLabel(testLabel));

console.log(x.shape, y.shape, tx.shape, ty.shape);

var pred: ml.Matrix = clf.predict(tx);
var accuracy: number = ml.Metrics.accuracy(ty, pred);

console.log(“accuracy: ” + accuracy.toString());
[/javascript]
* 実行結果

[ 120, 4 ] [ 120, 3 ] [ 30, 4 ] [ 30, 3 ]  ## 4次元 3クラス 学習データ数 120, テストデータ数 30
accuracy: 1  ## 正答率100%

irisデータセットは綺麗なデータなので良い分類結果が出ていますが、データ数が少ないので動作確認結果として妥当かどうか心配です。次にdigitsデータセットも同様に学習、テストを行ってみます。こちらはわかりやすい手書き数字認識のタスクとなります。
[javascript]
/*
var digits: {
data: number[][];
target: number[];
}
*/
var x1 = digits.data,
y1 = digits.target;

// 以下、同様に学習、テスト
[/javascript]
* 実行結果

[ 1438, 64 ] [ 1438, 10 ] [ 359, 64 ] [ 359, 10 ]  ## 64次元 10クラス 学習データ数 1438, テストデータ数 359
accuracy: 0.9217877094972067  ## 正答率約92%

約92%の正答率です。学習率のスケジューリングを丁寧にすればもっと精度は上がると思います。このくらいのデータ規模になると、学習には100イテレーションでも数秒かかりました。MNISTやCIFAR-10だとどれくらいの時間がかかるのでしょうか。

また、ユーティリティ系モジュールとして scikit-learn でいうところの LabelBinalier や metrics モジュールの機能を一部TypeScriptでも作りました。機械学習のアルゴリズムだけでなく、データ整形で必須となる機能が揃っているのも scikit-learn の便利なところ。ただ、重要な機能ではあるんですが移植作業をしていても全然楽しくないのが辛いです。

TypeScript 所感

このエントリーの主題はTypeScriptの勉強なので、この言語を使っていての個人的な所感を。

型のある安心感

機械学習で中心となる行列演算周りのコードを書いていると、メソッドの引数や戻り値がベクトルなのか行列なのかをきちんと検査してくれるのは安心感があります。Typed Arrayを使うプログラムだとありがたみがより大きいかもしれません。また、型推論のおかげで型指定をある程度省略できるのも助かります。

今回 interface は活用できていませんが、もし他の機械学習用のモジュールを追加する場合は使ってみようと思います。scikit-learnでも学習は fit、予測は predict のようにメソッド名が統一されているので真似するのが良さそうです。

コンパイルはそんなに遅くない

昔はTypeScriptのコンパイル(JavaScriptへのトランスパイル)がとても遅かったらしいですが、現在の僕の開発環境だと遅いという感覚はありません。また、Visual Studio Codeのエディタは軽快だし、インテリセンスの反応も良くサクサクと実装を進めることができます。
vscode_cap1

言語仕様面での課題

課題と書いてしまうと語弊がありますが、例えば機械学習だと行列の要素積などのelement-wiseな計算を頻繁に行うのですが、演算子をオーバーロードできる言語だと綺麗に(数式に近い感じで)実装しやすいです。現在のTypeScriptの仕様だと演算子のオーバーロードはできないので、メソッドチェーン方式でそれっぽい感じで書くしかないのでしょうか。醜いですけど、。Pythonでライブラリを使わずに一から実装しました系のエントリーはたくさん見かけますけど、思いっきりNumPy使ってるじゃないですか。羨ましい。

同じような感想を持っている方もいるようです。

アプリケーション開発者側ではオーバーロードは避けるべきなケースが多いかもしれませんが、アプリケーション開発者が使うライブラリの作成者側にとっては必要な機能だったりすることが多いのではないでしょうか。

おわりに

機械学習のアルゴリズムを実際に書いてみると、例えば損失の値がNaNになったり、ソフトマックス関数の計算中にexpの結果がオーバーフローしてInfに落ちたり、コンピュータで数値計算する際のハマり所にたくさん出会えるのでなかなか楽しいです。これは書籍やWebサイト上で数式を眺めているだけだと経験できないこと。数式と実装のギャップを埋めるための数値計算上のテクニックをいろいろ学ぶことが出来ます。

TypeScriptでの実装はPythonやC++で実装するより何倍も面倒なんですけど、結果としてたくさんコードが書けるので良い暇つぶしになるかなと思いました。あと、2011年にJavaScriptで決定木というアルゴリズムを書いたのですが、

これを発展させて勾配ブースティング(Gradient Boosting Decision Tree)を今度はTypeScriptで書いてみたいなと思っています。いきなり書くのは難しそうなので、Boostingに必要な部品から作ったらいいかなと考えています。

体系的な知識だけが増えて技術力が伴わない頭でっかちなエンジニアにはなりたくないので、ひたすら手を動かしてコードを書く習慣を付けたいと思います。さすがに2,3年ほど技術的なことから離れていたのでリハビリはもうしばらく続きそうです。

あわせて読む:

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です