前回は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にアップしていますので興味があれば。
ロジスティック回帰なので分類タスクとなります。まずは簡単な二値分類から。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// 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<N; i++) { x2[i][0] += 20; // 半分のデータ点を移動 x2[i][1] += 10; x4[i][0] += 30; x4[i][1] += 20; } var x: ml.Matrix = new ml.Matrix(x1.concat(x2)); // 行列のインスタンス作成 var y: ml.Matrix = new ml.Matrix(y1.concat(y2)); // ロジスティック回帰クラスのインスタンス作成 var clf: ml.LogisticRegression = new ml.LogisticRegression(x, y); var lr: number = 0.1, // 初期学習率 var l2Reg: number = 0.00, // L2正則化項係数 var iter: number = 100, // イテレーション回数 var verbose: boolean = true; // クロスエントロピー損失の値をコンソール出力 // 学習を実行 clf.fit(lr, iter, l2Reg, verbose); // テストデータ var tx: ml.Matrix = new ml.Matrix(x3.concat(x4)); var ty: ml.Matrix = new ml.Matrix(y3.concat(y4)); 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()); // データ作成用の関数 function createSample(d: number, n: number): number[][] { var data: number[][] = []; for (var i = 0; i < n; i++) { data[i] = []; for (var j = 0; j < d; j++) { data[i][j] = Math.floor(randn()); } } return data; } function createLabel(d: number, c: number, n: number): number[][] { var data: number[][] = []; for (var i = 0; i < n; i++) { data[i] = []; for (var j = 0; j < d; j++) { data[i][j] = 0; if (j == (c - 1)) { data[i][j] = 1; } } } return data; } // Box-Muller法 function randn(m = 0.0, v = 1.0): number { var a = 1 - Math.random(), b = 1 - Math.random(), c = Math.sqrt(-2 * Math.log(a)); if (0.5 - Math.random() > 0) { return c * Math.sin(Math.PI * 2 * b) * v + m; } else { return c * Math.cos(Math.PI * 2 * b) * v + m; } } |
* 出力結果
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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から読み込めるオブジェクトに変換しておきました。
- Iris Data Set (3クラス, 4次元特徴量)
- Handwritten Digits Data Set (10クラス, 64次元特徴量)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 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()); |
* 実行結果
1 2 |
[ 120, 4 ] [ 120, 3 ] [ 30, 4 ] [ 30, 3 ] ## 4次元 3クラス 学習データ数 120, テストデータ数 30 accuracy: 1 ## 正答率100% |
irisデータセットは綺麗なデータなので良い分類結果が出ていますが、データ数が少ないので動作確認結果として妥当かどうか心配です。次にdigitsデータセットも同様に学習、テストを行ってみます。こちらはわかりやすい手書き数字認識のタスクとなります。
1 2 3 4 5 6 7 8 9 10 |
/* var digits: { data: number[][]; target: number[]; } */ var x1 = digits.data, y1 = digits.target; // 以下、同様に学習、テスト |
* 実行結果
1 2 |
[ 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のエディタは軽快だし、インテリセンスの反応も良くサクサクと実装を進めることができます。
言語仕様面での課題
課題と書いてしまうと語弊がありますが、例えば機械学習だと行列の要素積などのelement-wiseな計算を頻繁に行うのですが、演算子をオーバーロードできる言語だと綺麗に(数式に近い感じで)実装しやすいです。現在のTypeScriptの仕様だと演算子のオーバーロードはできないので、メソッドチェーン方式でそれっぽい感じで書くしかないのでしょうか。醜いですけど、。Pythonでライブラリを使わずに一から実装しました系のエントリーはたくさん見かけますけど、思いっきりNumPy使ってるじゃないですか。羨ましい。
同じような感想を持っている方もいるようです。
アプリケーション開発者側ではオーバーロードは避けるべきなケースが多いかもしれませんが、アプリケーション開発者が使うライブラリの作成者側にとっては必要な機能だったりすることが多いのではないでしょうか。
おわりに
機械学習のアルゴリズムを実際に書いてみると、例えば損失の値がNaNになったり、ソフトマックス関数の計算中にexpの結果がオーバーフローしてInfに落ちたり、コンピュータで数値計算する際のハマり所にたくさん出会えるのでなかなか楽しいです。これは書籍やWebサイト上で数式を眺めているだけだと経験できないこと。数式と実装のギャップを埋めるための数値計算上のテクニックをいろいろ学ぶことが出来ます。
TypeScriptでの実装はPythonやC++で実装するより何倍も面倒なんですけど、結果としてたくさんコードが書けるので良い暇つぶしになるかなと思いました。あと、2011年にJavaScriptで決定木というアルゴリズムを書いたのですが、
これを発展させて勾配ブースティング(Gradient Boosting Decision Tree)を今度はTypeScriptで書いてみたいなと思っています。いきなり書くのは難しそうなので、Boostingに必要な部品から作ったらいいかなと考えています。
体系的な知識だけが増えて技術力が伴わない頭でっかちなエンジニアにはなりたくないので、ひたすら手を動かしてコードを書く習慣を付けたいと思います。さすがに2,3年ほど技術的なことから離れていたのでリハビリはもうしばらく続きそうです。