MNIST データセットをパースする
MNIST データセットは、THE MNIST DATABASE of handwritten digits からダウンロード可能な手書き数字のデータです。
機械学習ライブラリ等に標準で用意されてたりしますが、 今回は Node.js と Java でパースしてみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160307/
概要
MNIST は以下のようなデータです。
- 0 ~ 9 の手書き数字のグレースケール画像
- 1つの画像は 28x28 のサイズ
- 学習用に 6万件、テスト用に 1万件が用意されている
- 画像データとラベルデータのファイルが対になっている
下記のように 4種類のファイルが用意されています。
画像データファイル | ラベルデータファイル | |
---|---|---|
学習用 | train-images.idx3-ubyte | train-labels.idx1-ubyte |
テスト用 | t10k-images.idx3-ubyte | t10k-labels.idx1-ubyte |
ファイルフォーマットは以下の通りです。
画像データのファイルフォーマット
オフセット | タイプ | 値 | 内容 |
---|---|---|---|
0 | 32bit integer | 2051 | マジックナンバー |
4 | 32bit integer | 60000 or 10000 | 画像の数 |
8 | 32bit integer | 28 | 行の数 |
12 | 32bit integer | 28 | 列の数 |
16~ | unsigned byte | 0~255 ※ | 1バイトずつピクセル値が連続 |
先頭 16バイトがヘッダー部分、それ以降に 1画像 28 x 28 = 784 バイトのデータが 6万件もしくは 1万件続きます。
※ 0 が白、255 が黒
ラベルデータのファイルフォーマット
オフセット | タイプ | 値 | 内容 |
---|---|---|---|
0 | 32bit integer | 2049 | マジックナンバー |
4 | 32bit integer | 60000 or 10000 | ラベルの数 |
8~ | unsigned byte | 0~9 | 1バイトずつラベル値が連続 |
先頭 8バイトがヘッダー部分、それ以降にラベルデータが 6万件もしくは 1万件続きます。
(a) ConvNetJS 用に変換 (Node.js)
それでは、Node.js を使ってパースしてみます。
今回は ConvNetJS で使えるように、画像データを convnetjs.Vol
として作成するようにしました。
Buffer
を使ってバイナリデータを処理します。
MNIST のデータはビッグエンディアンのようなので 32bit integer を読み込む際は readInt32BE
を使います。
なお、マジックナンバーに関してはチェックしていません。
load_mnist.js
var Promise = require('bluebird'); var convnetjs = require('convnetjs'); var fs = require('fs'); var readFile = Promise.promisify(fs.readFile); // ファイル内容から Buffer 作成 var readToBuffer = file => readFile(file).then(r => new Buffer(r, 'binary')); // 画像データのロード var loadImages = file => readToBuffer(file) .then(buf => { var magicNum = buf.readInt32BE(0); var num = buf.readInt32BE(4); var rowNum = buf.readInt32BE(8); var colNum = buf.readInt32BE(12); // 画像データ部分を分離(ヘッダー部分を除外) var dataBuf = buf.slice(16); var res = Array(num); var offset = 0; for (var i = 0; i < num; i++) { var data = new convnetjs.Vol(colNum, rowNum, 1, 0); for (var y = 0; y < rowNum; y++) { for (var x = 0; x < colNum; x++) { var value = dataBuf.readUInt8(offset++); data.set(x, y, 0, value); } } res[i] = data; } return res; }); // ラベルデータのロード var loadLabels = file => readToBuffer(file) .then(buf => { var magicNum = buf.readInt32BE(0); var num = buf.readInt32BE(4); // ラベルデータ部分を分離(ヘッダー部分を除外) var dataBuf = buf.slice(8); var res = Array(num); for (var i = 0; i < num; i++) { res[i] = dataBuf.readUInt8(i); } return res; }); // 画像・ラベルデータをロード module.exports.loadMnist = (imgFile, labelFile) => Promise.all([ loadImages(imgFile), loadLabels(labelFile) ]).spread( (r1, r2) => r2.map((label, i) => { return { values: r1[i], label: label }; }) );
bluebird
と convnetjs
パッケージを使っています。
package.json
{ "name": "mnist_parse_sample", "version": "1.0.0", "description": "", "dependencies": { "bluebird": "^3.3.3", "convnetjs": "^0.3.0" } }
動作確認
以下のテストコードを使って簡単な動作確認を行います。
手書き数字の大まかな形状を確認できるように、ピクセル値が 0 より大きければ #
へ、それ以外は へ変換し出力してみました。
test_load_mnist.js
var mnist = require('./load_mnist'); var printData = d => { console.log(`***** number = ${d.label} *****`); var v = d.values; for (var y = 0; y < v.sy; y++) { var r = Array(v.sx); for (var x = 0; x < v.sx; x++) { // ピクセル値が 0より大きいと '#'、それ以外は ' ' r[x] = v.get(x, y, 0) > 0 ? '#' : ' '; } // 文字で表現した画像を出力 console.log(r.join('')); } // ピクセル値を出力 console.log(d.values.w.join(',')); }; mnist.loadMnist(process.argv[2], process.argv[3]) .then(ds => { console.log(`size: ${ds.length}`); printData(ds[0]); console.log('----------'); printData(ds[1]); });
MNIST 学習用データを使った実行結果は以下の通りです。
実行結果
> node test_load_mnist.js train-images.idx3-ubyte train-labels.idx1-ubyte size: 60000 ***** number = 5 ***** ############ ################ ################ ########### ####### ## ##### #### #### ###### ###### ###### ##### #### ####### ######## ######### ########## ########## ########## ######## 0,・・・,3,18,18,18,126,136,175,26,166,255,・・・,0 ---------- ***** number = 0 ***** ##### ###### ######### ########### ########### ############ ######### ### ###### ### ####### ### #### ### ### ### #### ### #### ##### ### ##### ### #### ### #### ############# ########### ######### ####### 0,・・・,51,159,253,159,50,・・・,0
(b) Deeplearning4J 用に変換 (Java)
次は Deeplearning4J で使えるように Java で org.nd4j.linalg.dataset.DataSet
へ変換します。
画像データとラベルデータをそれぞれ org.nd4j.linalg.api.ndarray.INDArray
として作成し、最後に DataSet へまとめます。
1件の画像データは 784 (= 28 x 28) 要素のフラットな INDArray として作成します ※。
※ Deeplearning4J では、ConvolutionLayerSetup を使って 画像の高さ・幅を指定できるため、フラットなデータにして問題ありません
ラベルの値は FeatureUtil.toOutcomeVector()
メソッドで作成します。
このメソッドによって、ラベルの種類と同じ数 (今回は 10) の要素を持ち、該当するインデックスの値だけが 1 で他は 0 になった配列 (INDArray) を得られます。
このように、変換後のデータセットの構造は (a) のケースとは異なります。
また、Java には unsigned byte
という型がなく、ピクセル値を byte として読み込むと -128 ~ 127 になってしまいます。そこで & 0xff
して正しい値 (0 ~ 255) となるように変換しています。
ちなみに、Deeplearning4J の org.deeplearning4j.datasets.DataSets.mnist()
メソッドを使えば MNIST データセットを取得できますが、その場合はピクセル値が正規化 ※ されて 0 か 1 の値となります。 (シャッフルも実施される)
※ ピクセル値 > 30 の場合は 1、それ以外は 0 になる
src/main/java/MnistLoader.java
import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.dataset.DataSet; import org.nd4j.linalg.factory.Nd4j; import org.nd4j.linalg.util.FeatureUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Paths; import java.util.concurrent.CompletableFuture; public class MnistLoader { private final static int LABELS_NUM = 10; public static CompletableFuture<DataSet> loadMnist(String imageFileName, String labelFileName) { return CompletableFuture.supplyAsync(() -> loadImages(imageFileName)) .thenCombineAsync( CompletableFuture.supplyAsync(() -> loadLabels(labelFileName)), DataSet::new ); } private static INDArray loadImages(String fileName) { try (FileChannel fc = FileChannel.open(Paths.get(fileName))) { ByteBuffer headerBuf = ByteBuffer.allocateDirect(16); // ヘッダー部分の読み込み fc.read(headerBuf); headerBuf.rewind(); int magicNum = headerBuf.getInt(); int num = headerBuf.getInt(); int rowNum = headerBuf.getInt(); int colNum = headerBuf.getInt(); ByteBuffer buf = ByteBuffer.allocateDirect(num * rowNum * colNum); // 画像データ部分の読み込み fc.read(buf); buf.rewind(); int dataSize = rowNum * colNum; INDArray res = Nd4j.create(num, dataSize); for (int n = 0; n < num; n++) { INDArray d = Nd4j.create(1, dataSize); for (int i = 0; i < dataSize; i++) { // & 0xff する事で unsigned の値へ変換 d.putScalar(i, buf.get() & 0xff); } res.putRow(n, d); } return res; } catch(IOException ex) { throw new RuntimeException(ex); } } private static INDArray loadLabels(String fileName) { try (FileChannel fc = FileChannel.open(Paths.get(fileName))) { ByteBuffer headerBuf = ByteBuffer.allocateDirect(8); // ヘッダー部分の読み込み fc.read(headerBuf); headerBuf.rewind(); int magicNum = headerBuf.getInt(); int num = headerBuf.getInt(); ByteBuffer buf = ByteBuffer.allocateDirect(num); // ラベルデータ部分の読み込み fc.read(buf); buf.rewind(); INDArray res = Nd4j.create(num, LABELS_NUM); for (int i = 0; i < num; i++) { res.putRow(i, FeatureUtil.toOutcomeVector(buf.get(), LABELS_NUM)); } return res; } catch(IOException ex) { throw new RuntimeException(ex); } } }
動作確認
Gradle で以下のテストコードを実行し簡単な動作確認を行います。
src/main/java/TestMnistLoader.java
import org.nd4j.linalg.api.ndarray.INDArray; import org.nd4j.linalg.dataset.DataSet; import java.util.stream.IntStream; public class TestMnistLoader { public static void main(String... args) { MnistLoader.loadMnist(args[0], args[1]) .thenAccept(ds -> { System.out.println("size: " + ds.numExamples()); printData(ds.get(0)); System.out.println("----------"); printData(ds.get(1)); }) .join(); } private static void printData(DataSet d) { System.out.println("***** labels = " + d.getLabels()); INDArray v = d.getFeatures(); IntStream.range(0, 28).forEach( y -> { IntStream.range(0, 28).forEach ( x -> { System.out.print( v.getInt(x + y * 28) > 0 ? "#" : " " ); }); System.out.println(); }); System.out.println(d.getFeatures()); } }
build.gradle
apply plugin: 'application' tasks.withType(AbstractCompile)*.options*.encoding = 'UTF-8' mainClassName = 'TestMnistLoader' repositories { jcenter() } dependencies { compile 'org.nd4j:nd4j-x86:0.4-rc3.8' runtime 'org.slf4j:slf4j-nop:1.7.18' } run { if (project.hasProperty('args')) { args project.args.split(' ') } }
MNIST 学習用データを使った実行結果は以下の通りです。
実行結果
> gradle run -Pargs="train-images.idx3-ubyte train-labels.idx1-ubyte" ・・・ :run 3 06, 2016 10:22:09 午後 com.github.fommil.netlib.BLAS <clinit> 警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS 3 06, 2016 10:22:10 午後 com.github.fommil.jni.JniLoader liberalLoad 情報: successfully loaded ・・・\Temp\jniloader211398199777152626netlib-native_ref-win-x86_64.dll **************************************************************** WARNING: COULD NOT LOAD NATIVE SYSTEM BLAS ND4J performance WILL be reduced Please install native BLAS library such as OpenBLAS or IntelMKL See http://nd4j.org/getstarted.html#open for further details **************************************************************** size: 60000 ***** labels = [ 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00, 0.00, 0.00, 0.00] ############ ################ ################ ########### ####### ## ##### #### #### ###### ###### ###### ##### #### ####### ######## ######### ########## ########## ########## ######## [ 0.00, ・・・, 3.00, 18.00, 18.00, 18.00, 126.00, 136.00, 175.00, 26.00, 166.00, 255.00, ・・・, 0.00] ---------- ***** labels = [ 1.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] ##### ###### ######### ########### ########### ############ ######### ### ###### ### ####### ### #### ### ### ### #### ### #### ##### ### ##### ### #### ### #### ############# ########### ######### ####### [ 0.00, ・・・, 51.00, 159.00, 253.00, 159.00, 50.00, ・・・, 0.00]
今回は DataSet を作っているだけなので WARNING を気にする必要は特にありませんが、WARNING を消して正しい状態で実行するには OpenBlas 等の BLAS ライブラリをインストールしてから実行します。
Windows 環境であれば、http://nd4j.org/getstarted.html#open のリンクから ND4J_Win64_OpenBLAS-v0.2.14.zip をダウンロード・解凍し、環境変数 PATH へ設定してから実行するのが簡単だと思います。
また、JniLoader が TEMP ディレクトリへ netlib-native_system-win-x86_64.dll
をダウンロードするのを防止するには、この dll も環境変数 PATH へ設定した場所へ配置しておきます。 (dll は Maven の Central Repository からダウンロードできます ※)
※ ダウンロードしたファイル名では都合が悪いようなので、 バージョン番号を除いた netlib-native_system-win-x86_64.dll という ファイル名へ変更します
OpenBlas 設定後の実行結果
> set PATH=C:\ND4J_Win64_OpenBLAS-v0.2.14;%PATH% > gradle run -Pargs="train-images.idx3-ubyte train-labels.idx1-ubyte" ・・・ :run 3 06, 2016 11:31:22 午後 com.github.fommil.jni.JniLoader liberalLoad 情報: successfully loaded ・・・\netlib-native_system-win-x86_64.dll size: 60000 ***** labels = [ 0.00, 0.00, 0.00, 0.00, 0.00, 1.00, 0.00, 0.00, 0.00, 0.00] ・・・
D3.js による折れ線グラフ SVG の作成と PNG 変換 - Node.js
前回、(ConvNetJS による処理結果を)折れ線グラフ化した方法に関して書いておきます。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160222/
準備
Node.js で D3.js を使うには、下記モジュールを npm でインストールします。
インストール例
> npm install d3 jsdom --save
D3.js は DOM を操作しますので、Node.js で使うには DOM を扱う jsdom のようなモジュールが必要となります。
(1) 折れ線グラフ(SVG)作成
前回は csv ファイルの内容をグラフ化しましたが、今回は変数定義した配列の値を折れ線グラフ化する事にします。 (配列の値を縦軸、配列のインデックス値 + 1 を横軸にします)
グラフ作成の手順は以下の通りです。
- (a) svg 要素の追加
- (b) スケールの定義
- (c) 軸の定義
- (d) 軸の追加
- (e) 折れ線の追加
(a) では console.log(document.body.innerHTML)
した内容 ※ が .svg ファイルとして (Web ブラウザ等で) そのまま表示できるように xmlns
を設定しています。
※ 今回、document.body へ追加した要素は svg 要素だけですので、 document.body.innerHTML は svg 要素の内容となります
また、今回は svg
要素の直下ではなく、translate を使って left と top のマージン分だけ位置をずらした g
要素の下にグラフを追加するようにしています。
(b) では、描画するデータの値の範囲(domain
で指定)を DOM 要素の幅・高さ(range
で指定)へマッピング(変換)する関数を作成します。
今回のグラフの場合、要素の高さとグラフの縦軸で数値の増加する方向が逆になるため (高さは上から下、縦軸は下から上)、高さ 0 の位置が縦軸の最大値となるようにマッピングしています。
(c) では axis
を使って縦軸や横軸を生成する関数を作成します。縦軸か横軸かは orient
へ指定した値で決定されるようです。
(d) では (c) の結果を call
へ指定する事で縦軸や横軸を追加できます。今回は text
要素を使って軸のラベルも追加しています。
あとは折れ線を作成する関数を定義し、path
要素の d 属性へ折れ線の作成結果を設定するだけです。(e)
line_chart.js
var d3 = require('d3'); var jsdom = require('jsdom').jsdom; var w = 400; // 幅 (マージン部分は除く) var h = 300; // 高さ (マージン部分は除く) var margin = { left: 50, top: 20, bottom: 50, right: 20 }; var xDomain = [0, 10]; // 横軸の値の範囲 var yDomain = [15, 0]; // 縦軸の値の範囲 var document = jsdom(); // データ var data = [3, 5, 4, 6, 9, 2, 8, 7, 5, 11]; // (a) svg 要素の追加 var g = d3.select(document.body).append('svg') .attr('xmlns', 'http://www.w3.org/2000/svg') .attr('width', w + margin.left + margin.right) // svg 要素の幅 .attr('height', h + margin.top + margin.bottom) // svg 要素の高さ .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); // (b) スケールの定義 var x = d3.scale.linear().range([0, w]).domain(xDomain); var y = d3.scale.linear().range([0, h]).domain(yDomain); // (c) 軸の定義 var xAxis = d3.svg.axis().scale(x).orient('bottom'); var yAxis = d3.svg.axis().scale(y).orient('left'); // (d1) 横軸の追加 g.append('g') .attr('transform', `translate(0, ${h})`) .call(xAxis) .append('text') // 横軸のラベル .attr('x', w / 2) .attr('y', 35) .text('番号'); // (d2) 縦軸の追加 g.append('g') .call(yAxis) .append('text') // 縦軸のラベル .attr('x', -h / 2) .attr('y', -30) .attr('transform', 'rotate(-90)') .text('個数'); // 折れ線の作成関数 var createLine = d3.svg.line() .x( (d, i) => x(i + 1) ) .y( d => y(d) ); // (e) 折れ線の追加 g.append('path') .attr('d', createLine(data)) .attr('stroke', 'blue') .attr('fill', 'none'); // SVG の出力 console.log(document.body.innerHTML);
実行結果は以下の通りです。
実行例
node line_chart.js > sample.svg
sample.svg の例 (実際は改行・字下げは無し)
<svg xmlns="http://www.w3.org/2000/svg" width="470" height="370"> <g transform="translate(50, 20)"> <g transform="translate(0, 300)"> ・・・ </g> ・・・ <!-- 折れ線部分 --> <path d="M40,240L80,200L120,219.99999999999997L160,180L200,120L240,260L280,140L320,160L360,200L400,80" stroke="blue" fill="none"></path> </g> </svg>
(2) SVG を PNG へ変換
次は、(1) で出力した SVG ファイルを PNG ファイルへ変換します。
変換する方法は色々あると思いますが、今回はコマンドラインで使用する事を前提にして下記を試してみました。
- (a) PhantomJS でキャプチャする
- (b) ImageMagick を使う
- (c) GraphicsMagick を使う
- (d) Inkscape を使う
(a) PhantomJS でキャプチャする
Web コンテンツを open
して render
する PhantomJS 用スクリプトを用意します。
capture.js
var system = require('system'); var webFile = system.args[1]; var pngFile = system.args[2]; var page = require('webpage').create(); page.open(webFile, function(status){ if (status == 'success') { // ページのキャプチャをファイルへ出力 page.render(pngFile); } phantom.exit(); });
phantomjs コマンドで以下のように実行すれば SVG ファイルを PNG ファイルへ変換できます。
実行例
> phantomjs capture.js sample.svg sample_phantom.png
sample_phantom.png
(b) ImageMagick を使う
convert コマンドで変換すると、文字化けしました。
実行例
> convert sample.svg sample_im.png
sample_im.png
-font オプションを使っても駄目でしたが、
以下のように SVG へ font-family
の style 設定を追加すると文字化けは解消しました。
line_chart_im.js
・・・ // 横軸の描画 svg.append('g') .attr('transform', `translate(0, ${h})`) .call(xAxis) .append('text') .attr('x', w / 2) .attr('y', 35) // ImageMagick による PNG 変換時の文字化け対策 .style('font-family', 'Sans') .text('番号'); // 縦軸の描画 svg.append('g') .call(yAxis) .append('text') .attr('x', -h / 2) .attr('y', -30) .attr('transform', 'rotate(-90)') // ImageMagick による PNG 変換時の文字化け対策 .style('font-family', 'Sans') .text('値'); ・・・
実行例2 (font-family 版)
> node line_chart_im.js > sample2.svg > convert sample2.svg sample2_im.png
sample2_im.png
(c) GraphicsMagick を使う
普通に gm convert
すると、gm convert: Unable to read font (n019003l.pfb)
というエラーが発生しました。 (Ghostscript をインストールしていない事が原因かも)
今回は -font でフォントを指定して回避しました。
実行例
> gm convert -font ipagp.ttf sample.svg sample_gm.png
sample_gm.png
(d) Inkscape を使う
Inkscape はベクター画像編集の GUI ツールですが、コマンドラインでも実行できます。 PNG ファイルへ変換するには -e オプションを使用します。
実行例
> inkscape -f sample.svg -e sample_ink.png
sample_ink.png
ConvNetJS で iris を分類
ニューラルネット用の JavaScript ライブラリ ConvNetJS を使って iris の分類を行ってみました。
ConvNetJS は Web ブラウザ上でも実行できますが、今回は Node.js で実行します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160215/
準備
iris データセットの取得
iris データセット (iris.data) は https://archive.ics.uci.edu/ml/datasets/iris からダウンロードできます。
iris.data の 151 行目(改行のみの行)は邪魔なので削除しておきます。
ConvNetJS インストール
今回は npm で ConvNetJS をインストールしました。
ConvNetJS インストール例
> npm install convnetjs --save
npm でインストールできるバージョンは今のところ 0.3.0 ですが、最新ソース と差異があるのでご注意ください。 (input で out_sx 等にデフォルト値が適用されない、adam に対応していない等)
また、最新ソースには bower.json が用意されていることから将来的に bower でインストールするようになるのかもしれません。
その他
今回は以下のライブラリも使いましたのでこれらもインストールしておきます。
> npm install basic-csv shuffle-array bluebird --save
(1) 単純な構成 (入力層 - 出力層)
まず、入力層と出力層だけの単純なニューラルネットの構成を試します。
層の構成
iris データセットの場合は、4つの変数を使って 3つの品種に分類する事になりますので、以下のように設定します。
- 入力層の out_depth へ変数の数 4 を設定 ※
- 出力層の type を softmax とし、num_classes で分類の数 3 を設定
※ out_sx と out_sy は画像データ等で使用するため 今回のようなケースでは 1 にします。 最新のソースでは、 out_sx と out_sy を省略できるようですが(デフォルト値 1 を適用) 0.3.0 では省略できないようですのでご注意下さい
convnetjs.Net
の makeLayers
を使ってニューラルネットを初期構築します。 (重みなどのパラメータをランダム値で初期化)
iris を分類するためのニューラルネット構築例(入力層 + 出力層)
// レイヤー構成の定義 var layer_defs = [ // 入力層 { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 }, // 出力層 { type: 'softmax', num_classes: 3 } ]; var model = new convnetjs.Net(); model.makeLayers(layer_defs);
出力層の活性化関数・誤差関数
今回のような分類を行う場合、出力層の活性化関数・誤差関数は一般的に以下を使うようです。
活性化関数 | 誤差関数 |
---|---|
ソフトマックス | 交差エントロピー |
ConvNetJS では出力層の type を softmax
とすればこれらを実施してくれます。(SoftmaxLayer の forward・backward に実装されています)
学習
学習は convnetjs.Trainer
を使って実施します。
train
メソッドへデータ 1件の入力値 (convnetjs.Vol
) と分類結果 (数値で指定可 ※) を与えると算出した誤差を基にパラメータを調整してくれます。
※ 3つに分類する場合は 0 ~ 2
Trainer に batch_size
(デフォルト 1) を設定すると、その回数毎にしかパラメータの調整を行わなくなり、ミニバッチを実現できます。
convnetjs.Vol
の引数へ配列を与えると sx=1, sy=1, depth=配列のサイズ, w=配列の内容
で初期化した Vol オブジェクトを取得できます。
テスト(評価)
テストデータの評価は、forward
へテストデータの入力値を与えた後、getPrediction
で取得した結果(分類の予測結果)と実際の値を比べる事で実施できます。
サンプル作成
学習係数などのハイパーパラメータにはデフォルト値を使い、iris のデータセットを学習用(105件)とテスト用(45件)に分割して、テスト用データの正解率を出力する処理を実装してみました。
学習を何度も繰り返す事でパラメータを最適化していきますので、以下では 20回繰り返すようにしています。
sample1.js
var Promise = require('bluebird'); var basicCsv = require('basic-csv'); var shuffle = require('shuffle-array'); var convnetjs = require('convnetjs'); var readCSV = Promise.promisify(basicCsv.readCSV); var epoch = 20; // 学習の繰り返し回数 var trainRate = 0.7; // 学習データとテストデータの分割率 var categoryMap = { 'Iris-setosa': 0, 'Iris-versicolor': 1, 'Iris-virginica': 2 }; // 階層型ニューラルネットの構成 var layer_defs = [ { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 }, { type: 'softmax', num_classes: 3 } ]; var model = new convnetjs.Net(); model.makeLayers(layer_defs); var trainer = new convnetjs.Trainer(model); readCSV('iris.data') .then( ds => // 入力データの作成 ds.map(d => new Object({ // 4つの変数を convnetjs.Vol 化 features: new convnetjs.Vol([ d[0], d[1], d[2], d[3] ]), // 分類の数値化 (0 ~ 2) label: categoryMap[d[4]] }) ) ) .then( ds => { shuffle(ds); var trainSize = Math.floor(ds.length * trainRate); var testData = ds.splice(trainSize); // 学習データとテストデータへ分割 return {train: ds, test: testData}; }) .then( data => { for (var i = 0; i < epoch; i++) { // 学習 data.train.forEach( d => trainer.train(d.features, d.label) ); // テスト(評価) var testAccSum = data.test.reduce( (acc, d) => { model.forward(d.features); return acc + (model.getPrediction() == d.label? 1: 0); }, 0); // テストデータの正解率を出力 console.log(testAccSum / data.test.length); } });
実行結果は以下の通りです。
実行するたびに初期パラメータや学習・テストデータの構成が変わりますので、結果も自ずと変化します。
実行結果1
> node sample1.js 0.6222222222222222 0.7777777777777778 0.6444444444444445 ・・・ 0.9777777777777777 0.9777777777777777 0.9777777777777777
実行結果2
> node sample1.js 0.8888888888888888 0.8888888888888888 0.9555555555555556 ・・・ 0.9555555555555556 0.9555555555555556 0.9333333333333333
(2) 誤差と正解率
次は、学習・テストそれぞれの誤差(の平均)と正解率を出力するようにしてみます。
更に、学習係数 learning_rate
と学習係数の調節手法 method
をコマンドライン引数で指定できるようにします。
method の値
0.3.0 で使える method の値は以下の通りです。(ただし、最新のソースでは adam 等が追加されている)
- sgd (デフォルト値)
- adagrad
- adadelta
- windowgrad
誤差
誤差というのはニューラルネットで算出された結果と実際の結果(正解値)との差異で、今回は交差エントロピーの結果です。
誤差は以下のようにして取得できます。
- 学習時は
train
の戻り値のloss
プロパティ - テスト時は
backward
の戻り値 (先にforward
を実行しておく必要あり)
誤差を最小化 (0 へ近づける) するようにパラメータが調整されていきますので、誤差の推移が重要です。
サンプル作成
sample1.js をベースにして、学習と評価の誤差と正解率を集計する処理を batchLearn
へ集約し、繰り返し回数を 50 に変更しています。
sample2.js
var Promise = require('bluebird'); var basicCsv = require('basic-csv'); var shuffle = require('shuffle-array'); var convnetjs = require('convnetjs'); var readCSV = Promise.promisify(basicCsv.readCSV); var epoch = 50; var trainRate = 0.7; var batchSize = 1; var learningRate = parseFloat(process.argv[2]); var updateMethod = process.argv[3]; ・・・ // 構成 var layer_defs = [ { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 }, { type: 'softmax', num_classes: 3 } ]; ・・・ var trainer = new convnetjs.Trainer(model, { batch_size: batchSize, learning_rate: learningRate, method: updateMethod }); // 学習・評価の実施 var batchLearn = (data, learnLossFunc) => { var res = data.reduce( (acc, d) => { // 誤差の加算 acc.loss += learnLossFunc(d); // 正解数の加算 acc.accuracy += (model.getPrediction() == d.label)? 1: 0; return acc; }, { loss: 0.0, accuracy: 0 } ); for (var key in res) { // 誤差・正解数の平均値を算出 res[key] /= data.length; } return res; }; readCSV('iris.data') ・・・ .then( data => { for (var i = 0; i < epoch; i++) { shuffle(data.train); // 学習 var trainRes = batchLearn(data.train, d => // 学習と誤差の取得 trainer.train(d.features, d.label).loss ); // テスト(評価) var testRes = batchLearn(data.test, d => { model.forward(d.features); // 誤差の取得 return model.backward(d.label); }); console.log([ trainRes.loss, trainRes.accuracy, testRes.loss, testRes.accuracy ].join(',')); } });
実行結果
learning_rate を 3パターン (0.05, 0.01, 0.001) と method を 3パターン (sgd, adagrad, adadelta) の組み合わせ (計 9パターン) の誤差と正解率を折れ線グラフで表示してみます。
- 青線が学習結果
- 赤線がテスト(評価)結果
グラフ作成には D3.js を使い、SVG として出力するようにしています。(line_chart.js 参照)
実行例
> node sample2.js 0.01 sgd > results/sample2-0.01-sgd.csv > node line_chart.js results/sample2-0.01-sgd.csv > results/sample2-0.01-sgd.csv.svg
実行の度に結果が変わる点にご注意下さい。
(a) method = sgd
learning_rate = 0.05 (sample2-0.05-sgd.csv.svg)
learning_rate = 0.01 (sample2-0.01-sgd.csv.svg)
learning_rate = 0.001 (sample2-0.001-sgd.csv.svg)
(b) method = adagrad
learning_rate = 0.05 (sample2-0.05-adagrad.csv.svg)
learning_rate = 0.01 (sample2-0.01-adagrad.csv.svg)
learning_rate = 0.001 (sample2-0.001-adagrad.csv.svg)
誤差が 1以下にならなかったので、誤差のグラフには何も描画されていません。
(c) method = adadelta
learning_rate = 0.05 (sample2-0.05-adadelta.csv.svg)
learning_rate = 0.01 (sample2-0.01-adadelta.csv.svg)
learning_rate = 0.001 (sample2-0.001-adadelta.csv.svg)
(3) 隠れ層を追加 (入力層 - 隠れ層 - 出力層)
最後に隠れ層を追加してみます。
サンプル作成
入力層と出力層の間に type: 'fc'
の層を追加すれば隠れ層を追加できます。
sample3.js
・・・ var fcNeuNum = parseInt(process.argv[4]); // 隠れ層のニューロン数 var fcAct = process.argv[5]; // 隠れ層の活性化関数 ・・・ // 構成 var layer_defs = [ { type: 'input', out_sx: 1, out_sy: 1, out_depth: 4 }, { type: 'fc', num_neurons: fcNeuNum, activation: fcAct }, { type: 'softmax', num_classes: 3 } ]; ・・・
実行結果
(2) と同様に誤差と正解率をグラフ化してみます。
隠れ層のニューロン数は 8、隠れ層の活性化関数は relu で実施しました。
実行例
> node sample3.js 0.01 sgd 8 relu > results/sample3-0.01-sgd-8-relu.csv > node line_chart.js results/sample3-0.01-sgd-8-relu.csv > results/sample3-0.01-sgd-8-relu.csv.svg
(a) method = sgd, num_neurons = 8, activation = relu
learning_rate = 0.05 (sample3-0.05-sgd-8-relu.csv.svg)
learning_rate = 0.01 (sample3-0.01-sgd-8-relu.csv.svg)
learning_rate = 0.001 (sample3-0.001-sgd-8-relu.csv.svg)
(b) method = adagrad, num_neurons = 8, activation = relu
learning_rate = 0.05 (sample3-0.05-adagrad-8-relu.csv.svg)
learning_rate = 0.01 (sample3-0.01-adagrad-8-relu.csv.svg)
learning_rate = 0.001 (sample3-0.001-adagrad-8-relu.csv.svg)
(c) method = adadelta, num_neurons = 8, activation = relu
learning_rate = 0.05 (sample3-0.05-adadelta-8-relu.csv.svg)
learning_rate = 0.01 (sample3-0.01-adadelta-8-relu.csv.svg)
learning_rate = 0.001 (sample3-0.001-adadelta-8-relu.csv.svg)
PureScript で DOM を操作
PureScript の下記ライブラリを使って簡単な DOM 操作を試してみました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160125/
はじめに
PureScript を使って実装するものと同等の処理を JavaScript で書いてみました。 id で指定した DOM ノードの textContent を変更するだけの簡単な処理です。
sample.js
var Sample = { updateContent: (id, content) => { var node = document.getElementById(id); if (node) { node.textContent = content; } } };
下記の HTML で実行してみます。 (PureScript の方は updateContent
の呼び出し部分が少し異なります)
index.html
<!DOCTYPE html> <html> <body> <h2 id="d"></h2> <script src="sample.js"></script> <script> Sample.updateContent('d', 'sample javascript'); </script> </body> </html>
実行結果
Web ブラウザで表示した結果は以下の通りです。
purescript-dom の場合
pulp init
でプロジェクトを作成し、pulp dep install
で purescript-dom
をインストールします。
なお、pulp と gulp を事前にインストール (npm install
) しておきます。
purescript-dom インストール
> pulp init ・・・ > pulp dep install purescript-dom --save
src/Main.purs を編集し updateContent
関数を実装します。
以下のように型まわりに注意が必要です。
- (a) document 関数は Eff (dom :: DOM | eff) HTMLDocument を返す
- (b) getElementById 関数の引数は ElementId と NonElementParentNode
- (c) getElementById 関数は Eff (dom :: DOM | eff) (Nullable Element) を返す
- (d) setTextContent 関数の引数は String と Node
(a) の結果の HTMLDocument を getElementById の引数へそのまま使えなかったので htmlDocumentToNonElementParentNode
関数で変換しています。
(c) の結果の Nullable はそのままだと使い難いので toMaybe
関数で Maybe 化し、Element も setTextContent
の引数に使えなかったので elementToNode
関数で変換しています。
src/Main.purs
module Main where import Prelude import Control.Monad.Eff import Data.Maybe import Data.Nullable (toMaybe) import DOM import DOM.HTML.Types import DOM.Node.Types import DOM.HTML (window) import DOM.HTML.Window (document) import DOM.Node.NonElementParentNode (getElementById) import DOM.Node.Node (setTextContent) updateContent :: forall eff. String -> String -> Eff (dom :: DOM | eff) Unit updateContent id content = do win <- window doc <- document win node <- getElementById (ElementId id) $ htmlDocumentToNonElementParentNode doc case (toMaybe node) of Just x -> setTextContent content (elementToNode x) _ -> return unit
今回は、gulp を使って pulp browserify
を実行するように以下のような gulpfile.js を用意しました。
Sample.updateContent
で関数を実行できるように --standalone
を指定しています。
gulpfile.js
var gulp = require('gulp'); var child_process = require('child_process'); var pulpCmd = (process.platform == 'win32')? 'pulp.cmd': 'pulp'; var destFile = 'sample.js' gulp.task('pulp_package', () => { // pulp browserify の実行 var res = child_process.spawnSync(pulpCmd, ['browserify', '--standalone', 'Sample', '-t', destFile]); // 実行結果の出力 [res.stdin, res.stdout, res.stderr].forEach( x => { if (x) { console.log(x.toString()); } }); }); gulp.task('default', ['pulp_package']);
gulp
コマンドを実行すると sample.js が生成されます。
ビルド例 (gulp で pulp browserify を実行)
> gulp
下記の HTML で実行してみます。
ここで、Sample.updateContent
はカリー化されており function(id) { return function(content) { return function __do() { ・・・ } } }
となっている点に注意。
index.html
<!DOCTYPE html> <html> <body> <h2 id="d"></h2> <script src="sample.js"></script> <script> Sample.updateContent('d')('sample purescript-dom')(); </script> </body> </html>
実行結果
purescript-simple-dom の場合
同じ様にして purescript-simple-dom
をインストールします。
purescript-simple-dom インストール
> pulp init ・・・ > pulp dep install purescript-simple-dom --save
src/Main.purs を編集し updateContent
関数を実装します。
purescript-dom と比べると余計な型変換が不要なのでシンプルです。
src/Main.purs
module Main where import Prelude import Control.Monad.Eff import DOM import Data.Maybe import Data.DOM.Simple.Window (document, globalWindow) import Data.DOM.Simple.Element (getElementById, setTextContent) updateContent :: forall eff. String -> String -> Eff (dom :: DOM | eff) Unit updateContent id content = do doc <- document globalWindow node <- getElementById id doc case node of Just x -> setTextContent content x _ -> return unit
purescript-dom と同じ様に gulp で sample.js を生成しました。(gulpfile.js は同じ内容です)
HTML は以下の通りです。
index.html
<!DOCTYPE html> <html> <body> <h2 id="d"></h2> <script src="sample.js"></script> <script> Sample.updateContent('d')('sample purescript-simple-dom')(); </script> </body> </html>
実行結果
virtual-dom のイベント処理
仮想 DOM を扱うための JavaScript 用ライブラリ virtual-dom では ev-xxx (例. ev-click
) でイベント処理を扱えるようになっていますが、実際に機能させるには dom-delegator が必要なようです。
virtual-dom は bower install が可能で Web ブラウザ上で直接使えるファイル (dist/virtual-dom.js) を用意してくれていますが、今のところ dom-delegator はそうなっていません。(dom-delegator は npm install する)
名称 | bower install | Web ブラウザで直接使用 |
---|---|---|
virtual-dom | ○ | ○ |
dom-delegator | × | × |
そこで、今回は下記ツールを用いて virtual-dom で ev-click
が機能するようにしてみます。
事前準備
Gulp・Bower・Browserify をインストールします。
インストール例
> npm install -g gulp bower browserify
virtual-dom のイベント処理サンプル
virtual-dom で ev-click を実施するサンプルを作成します。
ソースは http://github.com/fits/try_samples/tree/master/blog/20160107/
(1) virtual-dom インストール
virtual-dom を Bower でインストールします。
bower init と virtual-dom インストール例
> bower init ・・・ > bower install virtual-dom --save ・・・
(2) dom-delegator インストール
dom-delegator は npm でインストールします。 (dom-delegator は Bower でインストールできないため ※)
※ dom-delegator の git リポジトリを bower install する事は可能ですが、 その場合は依存ライブラリを手動でインストールする事になります
npm init と dom-delegator インストール例
> npm init ・・・ > npm install dom-delegator --save ・・・
(3) gulp-flatten インストール
Gulp でファイルをコピーする際にディレクトリ構成をフラット化するため gulp-flatten パッケージをインストールしておきます。
gulp-flatten インストール例
> npm install gulp-flatten --save-dev
Gulp で以下のように bower_components/virtual-dom/dist/virtual-dom.js を js ディレクトリへコピーすると、js/virtual-dom/dist/virtual-dom.js が作られます。
Gulp によるコピー例
gulp.task('js-copy', () => { gulp.src('bower_components/*/dist/*.js') .pipe(gulp.dest('js')); });
今回は、js/virtual-dom/dist/virtual-dom.js では無く js/virtual-dom.js へコピーしたかったので gulp-flatten を使いました。
(4) Gulp ビルド定義作成と実行
以下のような処理を行う gulpfile.js を作成します
- (a) bower_components/virtual-dom/dist/virtual-dom.js を js/virtual-dom.js へコピー
- (b) dom-delegator を Browserify で処理して js/dom-delegator.js へ出力 (Web ブラウザ上で DOMDelegator という名称で扱えるように standalone も設定)
gulpfile.js
var fs = require('fs'); var gulp = require('gulp'); var browserify = require('browserify'); var flatten = require('gulp-flatten'); // (a) gulp.task('js-copy', () => { gulp.src('bower_components/*/dist/*.js') .pipe(flatten()) // gulp-flatten でディレクトリをフラット化 .pipe(gulp.dest('js')); }); // (b) gulp.task('browserify', () => { browserify({ require: 'dom-delegator', standalone: 'DOMDelegator' }).bundle().pipe(fs.createWriteStream('js/dom-delegator.js')); }); gulp.task('default', ['js-copy', 'browserify']);
js ディレクトリを手動で作成した後、gulp
コマンドを実行します。
gulp 実行例
> mkdir js > gulp [00:19:17] Using gulpfile ・・・\virtual-dom\gulpfile.js [00:19:17] Starting 'js-copy'... [00:19:17] Finished 'js-copy' after 15 ms [00:19:17] Starting 'browserify'... [00:19:17] Finished 'browserify' after 17 ms [00:19:17] Starting 'default'... [00:19:17] Finished 'default' after 6.16 μs
これで下記のファイルが作成されます。
- js/virtual-dom.js
- js/dom-delegator.js
(5) virtual-dom の ev-click サンプル作成と実行
Gulp で生成した js ファイルを使い virtual-dom で ev-click を扱う単純なサンプルを作成し動作確認してみます。
virtual-dom は概ね以下のようにして使います。
- (1) virtualDom.diff 関数で 2つの VNode (VirtualNode) の差分を VPatch (VirtualPatch) として抽出
- (2) virtualDom.patch 関数で VPatch の内容を実際の DOM へ反映
index.html
<!DOCTYPE html> <html> <script src="js/virtual-dom.js"></script> <script src="js/dom-delegator.js"></script> <body> <h1>virtual-dom click sample</h1> <div id="ct"></div> <script> (function() { // ev-xxx イベントの有効化 (dom-delegator) new DOMDelegator(); var h = virtualDom.h; var create = virtualDom.create; var diff = virtualDom.diff; var patch = virtualDom.patch; var index = 1; var tree = h(); var render = function(v) { // 仮想 DOM の作成 return h('div', [ h('button', { // click イベントの設定 'ev-click': function(ev) { update(index++) } }, ['countUp'] ), h('br'), 'counter: ' + String(v) ]); } var update = function(v) { var newTree = render(v); // tree と newTree の差分を実際の DOM へ反映 patch(document.getElementById('ct'), diff(tree, newTree)); tree = newTree; }; update(index++); })(); </script> </body> </html>
Web ブラウザで実行し countUp ボタンを押下すると counter の数値がカウントアップする事を確認できました。
Web ブラウザ表示例
virtual-dom イベント処理の仕組み
最後に、virtual-dom と dom-delegator がどのようにイベント処理を行っているのか簡単に書いておきます。
まず、VNode の作成時に ev- で始まるプロパティは EvHook へ変換されます。(virtual-hyperscript/index.js の transformProperties)
patch 関数で実際の DOM へ反映する際に、対象の DOM ノードへ特殊なプロパティ __EV_STORE_KEY@7
※ を追加して EvHook の内容 (イベント名とイベント処理関数) を設定します。 (ev-store の機能)
※ プロパティ名は ev-store の index.js で定義されており、 ev-store 7.0.0 では __EV_STORE_KEY@7 となっています
__EV_STORE_KEY@7 が設定されている DOM ノード例
dom-delegator は "blur", "change", "click" 等の多数のイベントを document.documentElement で listen するように処理し、イベント発生時に対象 DOM ノードの __EV_STORE_KEY@7
プロパティの内容をチェックして、該当するイベントが設定されていればその処理関数を実行します。
つまり、上記のようなイベント処理を自前で行えば dom-delegator を使わなくても ev-click を機能させる事が可能です。
index2.html (dom-delegator を使わずに ev-click を有効化する例)
<!DOCTYPE html> <html> <script src="js/virtual-dom.js"></script> <body> <h1>virtual-dom click sample2</h1> <div id="ct"></div> <script> (function() { // dom-delegator の代用処理 document.addEventListener('click', function(ev) { var store = ev.target['__EV_STORE_KEY@7']; if (store && store[ev.type]) { // 該当するイベント処理関数の実行 store[ev.type](ev); } }, true); var h = virtualDom.h; ・・・ var render = function(v) { return h('div', [ h('button', { 'ev-click': function(ev) { update(index++) } }, ['countUp'] ), h('br'), 'counter: ' + String(v) ]); } ・・・ })(); </script> </body> </html>
pulp を使った PureScript の開発
PureScript 用のビルドツールに pulp があります。
pulp を使えば PureScript v0.7 から多少面倒になったビルドや実行が比較的容易になります。
pulp インストール
Node.js の npm で purescript (コンパイラ) と pulp をインストールします。
pulp インストール例
> npm install -g purescript pulp
今回インストールした PureScript コンパイラ・pulp のバージョンは以下の通りです。
なお、PureScript コンパイラに関しては https://github.com/purescript/purescript/releases/ から各 OS 用のバイナリを直接取得する方法もあります。
npm でインストールしたものも実際は node_modules/purescript/vendor
ディレクトリへ配置された各 OS 用のバイナリファイル (例. psc.exe) を使っているようです。
pulp を使った開発
今回作成したソースは http://github.com/fits/try_samples/tree/master/blog/20160105/
プロジェクトの作成
任意のディレクトリ内で pulp init
を実行すると、必要最小限のファイルが生成されます。
その際に Bower を使って PureScript の主要ライブラリ (以下) を自動的に取得しますので、git コマンドを使えるようにしておく必要があります。
- purescript-console
- purescript-eff
- purescript-prelude
プロジェクト作成例
> pulp init * Generating project skeleton in ・・・ bower cached git://github.com/purescript/purescript-console.git#0.1.1 bower validate 0.1.1 against git://github.com/purescript/purescript-console.git#^0.1.0 bower cached git://github.com/purescript/purescript-eff.git#0.1.2 bower validate 0.1.2 against git://github.com/purescript/purescript-eff.git#^0.1.0 bower cached git://github.com/purescript/purescript-prelude.git#0.1.3 bower validate 0.1.3 against git://github.com/purescript/purescript-prelude.git#^0.1.0 bower install purescript-console#0.1.1 bower install purescript-eff#0.1.2 bower install purescript-prelude#0.1.3 purescript-console#0.1.1 bower_components\purescript-console └── purescript-eff#0.1.2 purescript-eff#0.1.2 bower_components\purescript-eff └── purescript-prelude#0.1.3
ディレクトリ・ファイル構成は以下のようになります。
- bower_components
- purescript-console
- purescript-eff
- purescript-prelude
- src/Main.purs
- test/Main.purs
- .gitignore
- bower.json
デフォルトで用意されている src/Main.purs の内容は以下の通りです。
src/Main.purs
module Main where import Prelude import Control.Monad.Eff import Control.Monad.Eff.Console main :: forall e. Eff (console :: CONSOLE | e) Unit main = do log "Hello sailor!"
ライブラリの追加には pulp dep install <ライブラリ名>
を実行します。
そうすると bower install が実施されます。
bower.json の依存パッケージ設定へエントリを追加するには --save オプションを付けます。
purescript-tuples の追加例
> pulp dep install purescript-tuples --save ・・・ bower install purescript-control#0.3.2 bower install purescript-invariant#0.3.0 purescript-tuples#0.4.0 bower_components\purescript-tuples └── purescript-foldable-traversable#0.4.2 purescript-foldable-traversable#0.4.2 bower_components\purescript-foldable-traversable ├── purescript-bifunctors#0.4.0 └── purescript-maybe#0.3.5 purescript-maybe#0.3.5 bower_components\purescript-maybe └── purescript-monoid#0.3.2 purescript-bifunctors#0.4.0 bower_components\purescript-bifunctors └── purescript-control#0.3.2 purescript-monoid#0.3.2 bower_components\purescript-monoid ├── purescript-control#0.3.2 └── purescript-invariant#0.3.0 purescript-control#0.3.2 bower_components\purescript-control └── purescript-prelude#0.1.3 purescript-invariant#0.3.0 bower_components\purescript-invariant └── purescript-prelude#0.1.3
ビルドと実行
Main.purs を以下のようにタプルを使った処理に書き換えて実行してみます。
src/Main.purs
module Main where import Prelude import Control.Monad.Eff import Control.Monad.Eff.Console import Data.Tuple main :: forall e. Eff (console :: CONSOLE | e) Unit main = do let t = Tuple "two" 2 print t let r = t >>= \x -> Tuple " + 1" (x + 1) print r
pulp run
を実行するとビルドした後に処理を実施します。
ビルドだけを実施したい場合は pulp build
を使います。
実行
> pulp run * Building project in ・・・\20160105\purescript psc: No files found using pattern: src/**/*.js * Build successful. Tuple ("two") (2) Tuple ("two + 1") (3)
ビルド結果は output ディレクトリへ生成されます。
パッケージング
pulp browserify
を実行すると Browserify を使って output ディレクトリ内のファイルをパッケージングしてくれます。
browserify によるパッケージング
> pulp browserify > sample.js * Browserifying project in ・・・\20160105\purescript * Project unchanged; skipping build step. * Browserifying...
パッケージングしたファイル (sample.js) の実行結果は以下の通りです。
実行結果
> node sample.js Tuple ("two") (2) Tuple ("two + 1") (3)
Web ブラウザで実行する事もできます。
index.html
<!DOCTYPE html> <html> <script src="sample.js"></script> </html>
備考 - pulp を使わない場合
最後に pulp を使わない場合のビルド・実行方法も書いておきます。
まずは、Bower を使って purescript-console 等の必要なライブラリを手動でインストールします。
ライブラリのインストール例
> bower install purescript-console --save
src/Main.purs を psc コマンドでビルドするには以下のようにします。
psc によるビルド例
> psc src/Main.purs bower_components/purescript-*/src/**/*.purs --ffi bower_components/purescript-*/src/**/*.js
bower_components 内の .purs ファイルと --ffi オプションで bower_components 内の .js ファイルを指定します。
ビルド結果はデフォルトで output ディレクトリへ生成されます。(-o オプションで変更する事も可能)
実行する場合は、NODE_PATH 環境変数へ output ディレクトリを設定し、node コマンドで require('Main/index.js').main()
を実行します。
実行例
> set NODE_PATH=output > node -e "require('Main/index.js').main()" Tuple ("two") (2) Tuple ("two + 1") (3)
Google Cloud Print を Web API で操作 - Unirest 使用
リフレッシュトークンを使って Google Cloud Print を Web API で操作してみます。
以前、「Google アカウントで Google API を利用 - google-api-services-gmail」 では Apache HTTPClient を使いましたが、今回は Unirest を使っています。
ソースは http://github.com/fits/try_samples/tree/master/blog/20151222/
はじめに
事前準備としてリフレッシュトークンを取得しておきます。
今回は、「Google アカウントで・・・」 にて発行したクライアント ID をそのまま使用します。
(1) コードの取得
下記パラメータを付け https://accounts.google.com/o/oauth2/auth
へ Web ブラウザ等でアクセスし API の利用を許可する事でコードを取得します。
パラメータ | 値 |
---|---|
response_type | code |
client_id | クライアント ID の client_id 値(※1) |
scope | https://www.googleapis.com/auth/cloudprint |
redirect_uri | oob (※2) |
(※1)クライアント ID 発行時にダウンロードした JSON ファイルの client_id 値 (※2)リダイレクトしない場合の設定。 何らかのアプリケーション内から実行する際は それに合わせたリダイレクト先を指定する
例えば、以下のような URL へアクセスします。
URL 例
https://accounts.google.com/o/oauth2/auth?redirect_uri=oob&response_type=code&client_id=xxx.apps.googleusercontent.com&scope=https://www.googleapis.com/auth/cloudprint
Google アカウントでログインすると以下のような画面が表示されますので、「許可」ボタンを押下します。
コードが表示されるのでコピーしておきます。
(2) リフレッシュトークンの取得
次に、下記パラメータを https://www.googleapis.com/oauth2/v3/token
へ POST し、リフレッシュトークンを含んだ JSON を取得します。
パラメータ | 値 |
---|---|
code | (1) で取得したコード |
client_id | クライアント ID の client_id 値 |
client_secret | クライアント ID の client_secret 値 |
grant_type | authorization_code |
redirect_uri | oob (※1) |
(※1)(1) の場合と同様です。 ただし、以下のスクリプトでは urn:ietf:wg:oauth:2.0:oob を設定しています
Groovy スクリプト化すると以下のようになります。
get_refresh-token.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9') import com.mashape.unirest.http.Unirest import groovy.json.JsonSlurper addShutdownHook { Unirest.shutdown() } def json = new JsonSlurper() def conf = json.parse(new File(args[0])).installed def code = args[1] def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token') .field('code', code) .field('client_id', conf.client_id) .field('client_secret', conf.client_secret) .field('grant_type', 'authorization_code') .field('redirect_uri', conf.redirect_uris[0]) .asJson() println res.body
クライアント ID 発行時にダウンロードした JSON (ここでは client_secret.json) を第1引数、(1) で取得したコードを第2引数へ指定して実行します。
取得したリフレッシュトークン JSON は後で使うので保存しておきます。 (ここでは token.json へ保存)
実行例
> groovy get_refresh-token.groovy client_secret.json 4/9ic・・・ > token.json
プリンタ一覧の取得
リフレッシュトークンを使う場合は、以下のような手順で API を呼び出します。
- リフレッシュトークンからアクセストークンを取得
- アクセストークンを使って API を実行
リフレッシュトークンからアクセストークン取得
アクセストークンを取得するには、下記パラメータを https://www.googleapis.com/oauth2/v3/token
へ POST します。
パラメータ | 値 |
---|---|
client_id | クライアント ID の client_id 値 |
client_secret | クライアント ID の client_secret 値 |
grant_type | refresh_token |
refresh_token | リフレッシュトークンの値 (※1) |
(※1)リフレッシュトークン取得で保存した JSON の refresh_token 値
この処理は共通的に使用するため、Groovy の BaseScript として定義しました。
TokenScript.groovy
import com.mashape.unirest.http.Unirest import groovy.json.JsonSlurper abstract class TokenScript extends Script { // アクセストークンの取得 def accessToken(String clientId, String clientSecret, String refreshToken) { def res = Unirest.post('https://www.googleapis.com/oauth2/v3/token') .field('client_id', clientId) .field('client_secret', clientSecret) .field('grant_type', 'refresh_token') .field('refresh_token', refreshToken) .asJson() res.body.object } }
API 実行 (プリンタの一覧取得)
アクセストークンは HTTP ヘッダーへ Authorization: Bearer <アクセストークン>
のように設定して使います。(例. Authorization: Bearer ya26.pw・・・)
クラウドプリンタ一覧は https://www.google.com/cloudprint/search
へ GET すれば取得できます。 (q や type パラメータを付けて条件検索することも可能)
get_printers.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9') import com.mashape.unirest.http.Unirest import groovy.json.JsonSlurper import groovy.transform.BaseScript // TokenScript を BaseScript へ設定 @BaseScript TokenScript baseScript addShutdownHook { Unirest.shutdown() } def json = new JsonSlurper() def conf = json.parse(new File(args[0])).installed def token = json.parse(new File(args[1])) // アクセストークン取得 def newToken = accessToken( conf.client_id, conf.client_secret, token.refresh_token ) // API の実行 def printers = Unirest.get('https://www.google.com/cloudprint/search') .header('Authorization', "${newToken.token_type} ${newToken.access_token}") .asJson() println printers.body
実行結果は以下の通りです。
まだプリンタを登録していないためデフォルトの 「ドライブに保存」 しかありません。
実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy get_printers.groovy client_secret.json token.json { "request":{・・・}, "printers":[{ "isTosAccepted":false, "displayName":"ドライブに保存", "capsHash":"", "description":"ドキュメントを Google ドライブで PDF として保存します", "updateTime":"1370287324050", "ownerId":"cloudprinting@gmail.com", "type":"DRIVE", "tags":["save","docs","pdf","google","__google__drive_enabled"], "proxy":"google-wide", "ownerName":"Cloud Print", "createTime":"1311368403894", "defaultDisplayName":"ドライブに保存", "connectionStatus":"ONLINE", "name":"Save to Google Docs", "id":"__google__docs", "accessTime":"1316132041869", "status":"" }], "success":true, ・・・ }
また、https://www.google.com/cloudprint/list?proxy=<プロキシ>
へ GET すると、指定プロキシへ属するプリンタ一覧を取得できます。
印刷処理 (Google Drive へ保存)
最後に、「ドライブに保存」 プリンタへファイルを印刷してみます。 (実際は Google Drive へファイル保存)
印刷は、下記の必須パラメータ(他にもパラメータあり)を multipart/form-data
で https://www.google.com/cloudprint/submit
へ POST します。
パラメータ | 値 |
---|---|
printerid | プリンタ ID (※1) |
title | 印刷タイトル |
ticket | 印刷設定 (※2) |
content | 印刷するファイルの内容 |
(※1)今回は __google__docs を使用 (※2)CJT (Cloud Job Ticket) フォーマットで印刷オプションなどを指定
submit_file.groovy
@Grab('com.mashape.unirest:unirest-java:1.4.9') import com.mashape.unirest.http.Unirest import groovy.json.JsonSlurper import groovy.json.JsonBuilder import groovy.transform.BaseScript @BaseScript TokenScript baseScript addShutdownHook { Unirest.shutdown() } def ticketBuilder = new JsonBuilder() // ticket の内容 ticketBuilder ( version: '1.0', print: {} ) def json = new JsonSlurper() def conf = json.parse(new File(args[0])).installed def token = json.parse(new File(args[1])) def file = new File(args[2]) // アクセストークンの取得 def newToken = accessToken( conf.client_id, conf.client_secret, token.refresh_token ) // ticket の JSON 文字列化 def ticket = ticketBuilder.toString() // 印刷 def res = Unirest.post('https://www.google.com/cloudprint/submit') .header('Authorization', "${newToken.token_type} ${newToken.access_token}") .field('printerid', '__google__docs') .field('title', file.name) .field('ticket', ticket) .field('content', file) .asJson() println res.body
PDF ファイルを POST した結果は以下の通りです。
Google Drive で確認すると sample1.pdf ファイルが保存されていました。
実行結果 (出力結果は加工済、実際は改行・字下げは無し)
> groovy submit_file.groovy client_secret.json token.json data/sample1.pdf { "request":{・・・}, "success":true,"xsrf_token":"AIp・・・", "message":"印刷ジョブが追加されました。", "job":{ "ticketUrl":"https://www.google.com/cloudprint/ticket?format=xps&output=xml&jobid=0db・・・", "printerName":"", "errorCode":"", ・・・ "title":"sample1.pdf", "tags":["^own"], ・・・ "printerid":"__google__docs", ・・・ "contentType":"application/pdf", "status":"DONE" } }
また、試しに以下のような拡張子のファイルを POST してみたところ、自動的に PDF へ変換され Google Drive へ保存されました。 (例えば sample1.html は sample1.html.pdf で保存)
- .html
- .docx
- .odt