銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

コンソールからGoogle OAuth2の認証を行う

f:id:takamints:20171130164857j:plain
photo credit: suzyhazelwood DSC04299-02 via photopin (license)

コンソールから Node.jsを使って Google OAuth2 クライアントID による OAuth 認証のお試しコードを書いてみました。

日々の作業でGit BashやMSYS2を多用していますが、コンソールからGoogle Driveスプレッドシートを参照したり、検索、定型データの追加などが軽くできれば、いちいちブラウザアプリを開いて「よっこらしょ」ってな感じより効率的な場合もあるかと思い、これをベースに「なんか作るか」と、思ったり思わなかったりして、いくつになってもお勉強です。

JavaScriptエンジニアのためのNode.js入門
(2016-12-26)
売り上げランキング: 1,632

目次

関連記事:

takamints.hatenablog.jp

takamints.hatenablog.jp


Google OAuth2 に必要なもの

ここで示している認証では Google Developper ConsoleAPIプロジェクト(=アプリケーション)に作成する「クライアントID」と「クライアントシークレット」を使用します(作成手順は後述します)。

これとは別に「クライアントシークレット」を使わず「API KEY」と「クライアントID」を使う方法もあるようですが、Node.js からできるかどうかわかりませんでした。

そもそも OAuth の認証・認可とは

このアプリケーションは、Google Drive API を使用して、ユーザー自身の Google Drive のファイルにアクセスします(とプロジェクトに設定している)。しかしユーザーファイルを勝手に触るわけにはいきません。 このため、実行時に認証ページを表示して、「このアプリケーションがユーザーファイルにアクセスして良い」と、ユーザーに認可してもらうのです。

ユーザーが認可すれば、認可コードがアプリケーションに通知され、アプリケーションは実際にユーザーデータにアクセスするためのアクセストークを手に入れます。

認可を求めるのは1度だけ

認可を求める認証ページが開くのは初回の認可コードを取得するときだけです。

アクセストークンには有効期限が定められていますが、新たなアクセストークンを取得するためのリフレッシュトークも含まれており、2回目以降は、これを使って新しいアクセストークンを取得できるというわけ。

(掲載しているプログラムでは、有効期限の判定は一切行わず、常にリフレッシュしています)

認可コード取得を自動化

初回の認証シーケンス(認証ページの表示から認可コードの取得まで)は一般的なウェブアプリと同じように自動化しています。 通常のサンプルコード等では「以下のURLをブラウザで表示して、表示された認証コードを入力してね」なコピペな感じですが、それよか格段に楽ですよ。

認可コードを得る部分のフローは。

  1. プロセス内にローカルウェブサーバーを起動。
  2. 認証ページをユーザーのデフォルトブラウザで表示。
  3. 認証時のリダイレクト先を上記ウェブサーバーのURLに設定。
  4. ユーザーが認証を完了させると、認可コードがリダイレクト。

認可コードはHTTP GET REQUEST の QUERY_STRINGに含まれています。 だからウェブサーバーがリクエストを受け付けた時点で認可コードを取得できるというわけです。

プロジェクトとクライアントIDを作成する

以下の手順に従って、Google Developper ConsoleプロジェクトとクライアントIDを作成し、クライアントIDのキーファイルをダウンロードしておく必要があります。

  1. Google Developper Consoleで、新規プロジェクトを作成し、上のドロップダウンから作成したプロジェクトを選択。
  2. 「ライブラリ」タブで「Google Drive API」を使用できるように設定しておきます。検索ボックスに「Drive」と入力すればすぐ見つかる。認証をおこなうだけなら不要かもしれませんが、サンプルプログラムではGoogle Driveのルートにあるファイルの一覧を表示するために必要です。
  3. 「認証情報」タブで「OAuth 2.0 クライアント ID」を新規作成(「アプリケーションの種類」は「ウェブアプリケーション」か「その他」を選択。
  4. 作成したクライアントIDのJSONファイルをダウンロードします。

サンプルコード

以下のコードは、依存モジュールをインストールすれば単体で動作します。

ちょっと(いやかなり)長くなってしまい、かつコメント無しでスミマセン。

最初に定義されてるmain()が本体。ざっくり以下の様なことをやっています。

  1. コマンドラインでクライアントIDのキーファイルを(.jsonを抜いて)指定して実行します。
  2. キーファイルを読み込んでOAuth2のクライアントを作成。
  3. 初回の認証ではブラウザを開き、認可コードを受け取ります。
  4. 2回目以降の認証ではアクセストークンをリフレッシュしています。
  5. 何れにせよアクセストークンが得られたら、ファイルに保存し、
  6. 認可したアカウントのGoogle Driveのルートフォルダのファイル一覧を表示します。

※ 実際に動かすための情報は後述。

google-oauth2.js

"use strict";
const google = require("googleapis");
const OAuth2 = google.auth.OAuth2;
const drive = google.drive({ version: 'v3' });

const listit = require("list-it");
const opn = require('opn');
const server = require("node-http-server");
const fs = require("fs");

var REDIRECT_PORT = 8800;

var clientName = process.argv[2];

function main() {
    var clientFn = clientName + ".json";
    readJsonFile(clientFn).then(function(client) {
        console.log(clientFn + " Loaded:");
        console.log("Client:");
        console.log(JSON.stringify(client, null, "  "));
        return createAuth(client);
    }).then(function(auth) {
        var acctokFn = clientName + "-auth.json";
        return readJsonFile(acctokFn).then(function(acctok) {
            console.log(acctokFn + " loaded:");
            console.log("Access Token:");
            console.log(JSON.stringify(acctok, null, "  "));
            console.log("Refresh Tokens:");
            return refreshAccessToken(auth, acctok).then(function(acctok) {
                console.log(JSON.stringify(acctok, null, "  "));
                return writeJsonFile(acctokFn, acctok);
            }).then(function() {
                return auth;
            });
        }).catch(function(err) {
            console.log("No Access Token:");
            var authUrl = auth.generateAuthUrl({
                scope: "https://www.googleapis.com/auth/drive"
            });
            console.log("Auth URL:" + authUrl);
            return getAuthCode(authUrl, REDIRECT_PORT).then(function(code) {
                console.log("Auth Code:" + code);
                return getAccessToken(auth, code);
            }).then(function(acctok) {
                console.log("Access Token:");
                console.log(JSON.stringify(acctok, null, "  "));
                return writeJsonFile(acctokFn, acctok);
            }).then(function() {
                return auth;
            });
        });
    }).then(function(auth) {
        return getFileList({
            auth: auth,
            q: "parents='root' and trashed=false"
        });
    }).then(function(resp) {
        var list = listit.buffer({ "autoAlign" : true });
        list.d([ "name", "mimeType" ]);
        resp.files.forEach(function(file) {
            list.d([ file.name, file.mimeType ]);
        });
        console.log(list.toString());
    }).then(function() {
        process.exit(0);
    }).catch(function(err) {
        console.log("Error: ", err.message);
        process.exit(-1);
    });
}

function createAuth(client) {
    var credential;
    if("installed" in client) {
        credential = client.installed;
    } else if("web" in client) {
        credential = client.web;
    }
    var auth = new OAuth2(
            credential.client_id,
            credential.client_secret,
            "http://localhost:" + REDIRECT_PORT + "/");
    return auth;
}

function refreshAccessToken(auth, oldTokens) {
    return new Promise(function(resolve, reject) {
        auth.credentials = oldTokens;
        auth.refreshAccessToken(function(err, refreshedTokens) {
            if (err) {
                reject(err);
                return;
            }
            auth.credentials = refreshedTokens;
            resolve(refreshedTokens);
        });
    });
}

function getAccessToken(auth, code) {
    return new Promise(function(resolve, reject) {
        auth.getToken(code, function(err, tokens) {
            if (err) {
                reject(err);
                return;
            }
            auth.credentials = tokens;
            resolve(tokens);
        });
    });
}

function getFileList(params) {
    return new Promise(function(resolve, reject) {
        drive.files.list(params, function(err, resp) {
            if(err) {
                reject(err);
                return;
            }
            resolve(resp);
        });
    });
};

function getAuthCode(url, redirect_port) {
    opn(url);
    return waitHttpRequest(redirect_port, "/").then(function(http) {
        var code = http.request.uri.query.code;
        http.serve(http.request, http.response,
            ["Auth Code:", code].join(" "));
        return code;
    });
}

function waitHttpRequest(port, pathname) {
    return new Promise(function(resolve, reject) {
        var config = new server.Config();
        config.port = port;
        server.onRequest = function(request, response, serve) {
            if(request.uri.pathname == pathname) {
                resolve({
                    request: request,
                    response: response,
                    serve: serve
                });
                return true;
            } else {
                reject(new Error("Illegal access."));
                return false;
            }
        };
        server.deploy(config);
    });
}

function readJsonFile(fn) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fn, function(err, data) {
            if(err) {
                reject(err);
                return;
            }
            resolve(JSON.parse(data));
        });
    });
}

function writeJsonFile(fn, obj) {
    return new Promise(function(resolve, reject) {
        fs.writeFile( fn, JSON.stringify(obj, null, "    "), function(err) {
            if(err) {
                reject(err);
                return;
            }
            resolve();
        });
    });
}

main();

依存モジュールのインストール

依存モジュールは以下4つ。

これらが利用可能な状態でなければ動きません。npmでインストールして下さい。 npm install googleapis list-it opn node-http-server をコピペでいけます。

実行例

コマンドライン引数に、クライアントIDのキーファイルを指定して実行します。 以下実行例で、キーファイルは ./client_id.jsonとして保存されています。

ファイル一覧の桁がエラくズレていますが、実際のコンソールでは問題なしよ。 (デバッグ出力が長くて申し訳ない)

初回の認証実行例

$ node google-oauth2.js ./client_id
./client_id.json Loaded:
Client:
{
  "installed": {
    "client_id": "<クライアントID>",
    "project_id": "<プロジェクト名>",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "<クライアントシークレット>",
    "redirect_uris": [
      "urn:ietf:wg:oauth:2.0:oob",
      "http://localhost"
    ]
  }
}
No Access Token:
Auth URL:<認証URL>
Auth Code:<認可コード>
Access Token:
{
  "access_token": "<アクセストークン>",
  "refresh_token": "<リフレッシュトークン>",
  "token_type": "Bearer",
  "expiry_date": 1512041811335
}
name                   mimeType
******                   application/vnd.google-apps.spreadsheet
******                   application/vnd.google-apps.folder
******                   application/vnd.google-apps.script
******                   application/vnd.google-apps.folder
Document           application/vnd.google-apps.folder
Image                  application/vnd.google-apps.folder
Google フォト    application/vnd.google-apps.folder
My Tracks            application/vnd.google-apps.folder

$

2回目以降のリフレッシュトークンを利用した実行例

$ node google-oauth2.js ./client_id
./client_id.json Loaded:
Client:
{
  "installed": {
    "client_id": "<クライアントID>",
    "project_id": "<プロジェクト名>",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "<クライアントシークレット>",
    "redirect_uris": [
      "urn:ietf:wg:oauth:2.0:oob",
      "http://localhost"
    ]
  }
}
./client_id-auth.json loaded:
Access Token:
{
  "access_token": "<アクセストークン>",
  "token_type": "Bearer",
  "expiry_date": 1512021213468,
  "refresh_token": "<リフレッシュトークン>"
}
Refresh Tokens:
{
  "access_token": "<アクセストークン>",
  "token_type": "Bearer",
  "expiry_date": 1512040667695,
  "refresh_token": "<リフレッシュトークン>"
}
name                   mimeType
******                   application/vnd.google-apps.spreadsheet
******                   application/vnd.google-apps.folder
******                   application/vnd.google-apps.script
******                   application/vnd.google-apps.folder
Document           application/vnd.google-apps.folder
Image                  application/vnd.google-apps.folder
Google フォト    application/vnd.google-apps.folder
My Tracks            application/vnd.google-apps.folder

$

まとめ

とりあえず認証が通ったなら、あとは各種APIを利用して、いろんなことができるので、おらワクワクすっぞ!

もう少しキチンとライブラリ的にまとめられたら独立したnpmとしてpublishするつもりですが、今の段階ではこの状態でごめんなさい。

参考サイト

線形回帰で「アヤメ(iris)」の分類

f:id:takamints:20171015205935j:plain

octaveを使って線形回帰で「アヤメの分類」をやってみました。

なんで急にこんなことやり始めたかっていうと、先日、会社の方に初めて機械学習関連のお仕事が舞い込んできたようでして、自分は担当外なのですが「今の時代、それぐらいできなアカンやろーwww」とか、いつもの調子でついハッタリをかましてしまって(笑)・・・、
不安になって自宅で機械学習のコソ練せざるを得ないという。いくつになってもお勉強です(泣)

「線形回帰てなんやねん」については以下の記事で、ごく初歩の概念を説明してます。
takamints.hatenablog.jp

お仕事の方は、Chainer/XGBoost/Pythonという条件がついているようですが、我が師と(勝手に)仰いでいる Andrew Ng 先生は「最初は octave(matlab) でやりなはれや」とおっしゃられていたので、手元の octave でやってみましたという次第。

ということで、以降のスクリプトを実行するには octave または matlabが必要です。 データセットのダウンロードと変換に curl と Nodeを使っていますが、手動でやるなら不要ですよ。

octaveWindowsへの導入は、以下の記事を参考に。

takamints.hatenablog.jp

Chainer v2による実践深層学習
新納 浩幸
オーム社
売り上げランキング: 4,654
MATLABとOctaveによる科学技術計算

丸善出版
売り上げランキング: 130,528

まずはアヤメ(iris)をダウンロードして変換します

機械学習のためのオープンな訓練データとして、アヤメ(iris)というのがあるらしいと、この度初耳。

データセットは150件と小さくて、4つのパラメータで3種類のアヤメを同定するというものです。

上記のファイルをダウンロードするため以下のシェルスクリプトを書きました。

get-iris-csv.sh

#!/bin/sh
echo Downloading iris.data and iris.names.
curl --silent https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data > iris.data
curl --silent https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.names > iris.names
echo
echo Converting iris.data CSV.
echo
node conv-iris-csv.js

iris.dataをダウンロードしてCSVを変換しています(変換については次項↓参照)。

bashで実行するとこうなります。

$ ./get-iris-csv.sh
Downloading iris.data and iris.names.

Converting iris.data CSV.

The class values in column 5:
0: Iris-setosa
1: Iris-versicolor
2: Iris-virginica

CSVの5列目を数値に変換

iris.data の5列目はアヤメの種類の名前であり、文字列です。 しかし、octave の csvread 関数では数値しか読み込めません。数学的な行列ですからね。

そこで以下のNodeスクリプト。 iris.data を読み込んで、全行の5列目の文字列を 0,1,2 の数値に変換。 iris.csv に出力してます(おっと、エラーを一切見ていませんが良い子は絶対マネしないでw)。

conv-iris-csv.js

#!/usr/bin/env node
var inputFile = "iris.data";
var outputFile = "iris.csv";
var fs = require("fs");
fs.readFile(inputFile, "utf-8", function(err, data) {
    var indexOfName = {};
    fs.writeFile(outputFile, data.split(/\r*\n/).map(function(row, indexRow) {
        if(row == "") {
            return "";
        }
        return row.split(",").map(function(column, indexCol) {
            if(indexCol < 4) {
                return column;
            }
            if(!(column in indexOfName)) {
                indexOfName[column] = Object.keys(indexOfName).length;
            }
            return indexOfName[column];
        }).join(",");
    }).join("\n"),
    function(err) {
        var nameIndex = new Array(Object.keys(indexOfName).length);
        Object.keys(indexOfName).forEach(function(key) {
            nameIndex[indexOfName[key]] = "" + indexOfName[key] + ": " + key;
        });
        console.log("The class values in column 5:");
        console.log(nameIndex.join("\n"));
    });
});

データについて

iris.namesの「4. Relevant Information」には、 「ひとつは他の2つからリニアに分離できるが、他の2つは無理」と書いてありました(以下1>の行)

また、「データに間違いがある」とも書かれていました(以下2>の行)が、修正しても結果に変化はありませんでした。

4. Relevant Information:
   --- This is perhaps the best known database to be found in the pattern
       recognition literature.  Fisher's paper is a classic in the field
       and is referenced frequently to this day.  (See Duda & Hart, for
       example.)  The data set contains 3 classes of 50 instances each,
1>       where each class refers to a type of iris plant.  One class is
1>       linearly separable from the other 2; the latter are NOT linearly
1>       separable from each other.
   --- Predicted attribute: class of iris plant.
   --- This is an exceedingly simple domain.
   --- This data differs from the data presented in Fishers article
    (identified by Steve Chadwick,  spchadwick@espeedaz.net )
2>   The 35th sample should be: 4.9,3.1,1.5,0.2,"Iris-setosa"
2>   where the error is in the fourth feature.
2>   The 38th sample: 4.9,3.6,1.4,0.1,"Iris-setosa"
2>   where the errors are in the second and third features.  ```

線形回帰のoctave/matlabスクリプト

以下のoctave/matlabスクリプトが今回の御本尊。

  1. CSVを読み込んで、
  2. 学習してから
  3. 検証してます。

本来ならばデータセットの3分の1程度をトレーニングには使わず検証用に残しておくべきなのですが、 データ件数が150件と非常に小さいため、全データでトレーニングして、全データで検証してます。手前味噌な感じですが仕方がない。

iris.m

#
# Classification of iris using linear regression
#

# Clear all mat
clear

D = csvread('iris.csv');        # Load iris.csv
# D = D(randperm(size(D,1)),:); # Sort random

#
# Select training dataset
#
Dt = D(1:size(D, 1), :);    # Dt = D(1:2*size(D, 1)/3, :);
Xt = Dt(:, 1:(size(Dt,2)-1));    # Input
Xt = [ones(size(Xt,1),1) Xt];
Yt = Dt(:, size(Dt,2));      # Result

#
# Select validation dataset
#
Dv = D(1:size(D, 1), :);    # Dv = D(2*size(D, 1)/3+1:size(D,1), :);
Xv = Dv(:, 1:(size(Dv,2)-1));
Xv = [ones(size(Xv,1),1) Xv];
Yv = Dv(:, size(Dv,2));

training_data_count = size(Xt,1);
learning_rate = 0.00001     # learning rate
iteration = 5000000
report_interval = round(iteration / 10);

#
# Training
#
theta = ones(1, size(Xt,2)); # factor
numOfParam = size(theta, 2);
update = [1, numOfParam];
for n = 1:iteration
    diff = Xt * theta' - Yt;
    for j = 1:numOfParam
        update(1,j) = diff' * Xt(:,j);
    endfor
    theta = theta - learning_rate * update / training_data_count;
    if mod(n,report_interval) == 0
        cost = (diff' * diff) / ( 2 * size(Xt,1) )
    endif
endfor

#
# Validation
#
y = Xv * theta';
diff = y - Yv;
#validationResult = [Yv round(y)]

error_count = 0;
for i = 1:size(diff,1)
    diffI = diff(i,1);
    if(diffI * diffI >= 0.5 * 0.5)
        error_index = i
        error_count = error_count + 1;
    endif
endfor
theta
validationErrorRate = error_count / size(Xv, 1)
validationTotalCost = diff' * diff / ( 2 * size(Xv,1) )

結果検証

以下が上記スクリプトの実行結果。 150件中3件の判定間違いがありますが、これが限界みたいです。

learning_rate = 1.0000e-005
iteration =  5000000
cost =  0.026944
cost =  0.025440
cost =  0.024725
cost =  0.024366
cost =  0.024169
cost =  0.024048
cost =  0.023963
cost =  0.023897
cost =  0.023841
cost =  0.023792
error_index =  71
error_index =  84
error_index =  134
theta =

   0.586249  -0.177805  -0.067764   0.244015   0.621859

validationErrorRate =  0.020000
validationTotalCost =  0.023792

(2017-10-21追記①)

データの可視化をしようとしていて、単なる偶然ですが学習回数を少なくできるのを見つけました。 上の実行例では学習を500万回繰り返していますが、1万回で同じ結果を得られます。 learning_rateは1000倍で、学習は数秒で終わります。さらに誤差が若干少ない。

iris
learning_rate =  0.010000
iteration =  10000
cost =  0.025441
cost =  0.024366
cost =  0.024048
cost =  0.023897
cost =  0.023792
cost =  0.023708
cost =  0.023636
cost =  0.023575
cost =  0.023522
cost =  0.023476
error_index =  71
error_index =  84
error_index =  134
theta =

   0.464549  -0.152014  -0.066062   0.234802   0.621481

validationErrorRate =  0.020000
validationTotalCost =  0.023476

(2017-10-21追記①)おわり

別途「Iris-setosa」を、他の2つから完璧に(そしてかなり簡単に)分離できました。 しかし「Iris-versicolor」と「Iris-virginica」はパラメータ平面において数件が領域を共有してるため、線形モデルで分離するのは困難なようです。

これ以上判定精度を挙げるには別の新たなパラメータが必要になるはずです。 そんなこんなで、この分類は成功していると言えそうです。 当初はなんかおかしいと思っていましたが(笑)

結果が離散的な分類問題なので、ロジスティック回帰で解く必要があるのかも?と思いましたが、線形モデルである限りは結果は変わらないはずです。

また、ディープラーニングではキレイに分離できるはずですが、しかし、おそらくそれはオーバーフィッティングであって、一般的な解とはいえない(つまり別のデータセットを持ってきたら、やはり間違う)はず。そして、学習には膨大な中間層が必要で時間もかかると思います。

(2017-10-21追記②)

入力データと結果の可視化

入力データと判定エラーのチャートを描いてみました。 (赤:Setona、緑:Versicolour、青:Virginica)

f:id:takamints:20171021124312p:plain

○が入力データで、●は判定ミスです(間違ってこの色が示す種類に判定された)。

ご覧のように、緑:Versicolour、青:Virginicaとが入り混じっている箇所があります。 この部分がリニアには分離できないと書かれているところだと思います。

ちなみにチャートを描くスクリプトは、以下になります。

# Draw iris charts
[];

function show_chart(D, error_data, featureX, featureY, chartTitle)

    hold all

    Dy0 = D( D( :, 5 ) == 0, : );
    scatter(Dy0(:,featureX), Dy0(:,featureY), 'r')

    Dy1 = D( D( :, 5 ) == 1, : );
    scatter(Dy1(:,featureX), Dy1(:,featureY), 'g')

    Dy2 = D( D( :, 5 ) == 2, : );
    scatter(Dy2(:,featureX), Dy2(:,featureY), 'b')

    error_data_r = error_data(error_data(:,5)==0, :);
    scatter(error_data_r(:, featureX), error_data_r(:, featureY), [], 'r', 'filled')

    error_data_g = error_data(error_data(:,5)==1, :);
    scatter(error_data_g(:, featureX), error_data_g(:, featureY), [], 'g', 'filled')

    error_data_b = error_data(error_data(:,5)==2, :);
    scatter(error_data_b(:, featureX), error_data_b(:, featureY), [], 'b', 'filled')

    title(chartTitle)

endfunction

subplot(1,2,1)
show_chart(D, error_data, 1, 2, 'X:Sepal length, Y:Sepal width')

subplot(1,2,2)
show_chart(D, error_data, 3, 4, 'X:Petal length, Y:Petal width')

(2017-10-21追記②)おわり

公開リポジトリ

上記のスクリプトは、以下のGitリポジトリに置いてます。 (最新版では学習回数は1万回に設定されています)。 試してみたい人は是非どうぞ。

github.com

データ解析のためのロジスティック回帰モデル
Jr David W. Hosmer Stanley Lemeshow Rodney X. Sturdivant
共立出版
売り上げランキング: 178,009

C++でJsonを扱うクラスライブラリ jsonexpr

8月から支援に入った画像処理(リアルタイム・オブジェクト・トラッキング)プロジェクトの検証システムで、C++の本体からJSONを吐く必要がありまして、急いでやっつけたのですが、「そういや昔、C++JSON扱うクラスライブラリ作ったね・・・」と思い出しました(遅い)。

ということで、古いGitHubから引っ張り出して確認すると、どうにか使えそうなので、この場を借りて宣伝します(笑)

f:id:takamints:20170924182608p:plain

実践OpenCV 3 for C++画像映像情報処理
永田 雅人 豊沢 聡
カットシステム
売り上げランキング: 263,159

元々 Node.jsで出力したJSONファイルをC++で読み込んで処理させる目的で作ったものです。 当時、ニューラルネットワークバックプロパゲーションを理解するための実験を何故かC++で書いていましたが、Node.jsで大量に生成した訓練用データを入力する必要があったのです。結局本体もJavaScriptに移植したので存在を忘れてた。

C++で学ぶディープラーニング
藤田 毅
マイナビ出版 (2017-06-26)
売り上げランキング: 192,886

機能

JavaScriptの変数と同じように扱うためのjson::varというクラスが、ほぼすべての機能を含んでいます。他のクラスはこのクラスの内部で使用するものです。

ビルドについて

ビルドはCMakeを使っています。手元の環境では、Visual Studio 2015のソリューションは生成できました。Mingw32のG++用スクリプトが入っていますがエラーになります(原因は追求していません)。Linuxでは試していませんが、またそのうち。

ライセンス

ライセンスを明記していなかったので急遽MITライセンスを謳っておきました。

リポジトリはコチラです

github.com

同カテゴリでは picojson など著名なライブラリがいくつかあるようですが、興味のある方は試してみて下さい。 今見ると少し手を入れたい所がありますが、取り急ぎ現状のままお送りします。

以下、READMEを貼っておきます。

jsonexpr

JSONC++で自然に扱うために作ったライブラリです。

json::var クラスのインスタンスが、JavaScript のデータ型(Number, boolean, string, Array, Object)に相当します。

データ型

  1. 数値 - double型の数値
  2. 文字列 - std::string型の文字列
  3. 配列 - json::var型の配列
  4. オブジェクト - 文字列からjson::var型へのディクショナリ

初期化

json::var のコンストラクタにJSON文字列を与えて初期化できます。 プリミティブ型ではあとで説明していますが、代入するほうが直感的かも知れませんね。

//数値の初期化(内部表現は全てdouble)
json::var real("-1.234e+5");  //浮動小数点
json::var dec("1234");        //10進整数
json::var hex("0x1234");      //16進整数
json::var oct("0644");        //8進整数

//文字列で初期化
json::var str("'string'");    //シングルクォート

//配列の初期化
json::var arr("[-1.234e+5,'string']");

//オブジェクトの初期化
json::var obj("{'key':'value', foo: 'bar', arr:[1,2,3,4]}");

配列操作

  1. length() - 要素数を得る
  2. push(値) - 要素の追加。値は数値、文字列、配列、オブジェクト
  3. remove(index) - 要素の削除。
json::var arr("[]");//空の配列
arr.push(1.0);      //数値を追加
arr.push("string"); //文字列を追加
for(int i = 0; i < arr.length(); i++) {
    cout << arr[i];
}

オブジェクト操作

  1. exists(key) - キーの有無を調べる
  2. keys() - キーの配列を返す。
  3. remove(key) - キーの削除。

新たなキーに値を関連付けるには、[]で直接代入します。 ※ 定数でないoperator[](const std::string&)では、参照するだけでキーが作成されることに注意。

json::var obj("{}");    //空のオブジェクト
obj["A"] = "B";         //文字列の値を追加
obj["R"] = arr;         //既にある配列のコピーを追加
json::var keys = obj.keys();
for(int i = 0; i < keys.length(); i++) {
    cout << "#" << keys[i] << " => " << obj[keys[i]] << endl;
}

値の参照

数値型は、doubleへのキャスト、文字列型は std::string&へのキャストで値を参照します。

配列要素とオブジェクトの値は[]を使用。配列要素は添え字が整数、オブジェクトは文字列型。 ネストしたオブジェクトは二次元配列のようにかぎかっこ([])をつないで参照します。

json::var real = -1.234e+5;
double r = (double)real;     // r == -1.234e+5;

json::var str = "ABC";
std::string s = (const std::string&)str;

json::var element = arr[3];             //配列要素の参照
json::var value = obj["arr"][0];      //オブジェクト内の配列要素の参照

内部表現と矛盾する参照を行うと、例外が投入されます。

値の代入

data = 1.234;           //数値を代入
data = "string";        //文字列を代入
arr[3] = 1.234;         //配列要素の書き換え
obj["key"] = 1.234;     //オブジェクト要素の書き換え
obj["new key"] = arr;   //オブジェクトへ新たな項目を追加

JSONデータのストリーム入出力

C++の標準ストリーム入出力機能を実装しています。

std::istream からのJSONの読み込み

json::var dataobj;
ifstream is("input.json");
is >> dataobj;

std::ostreamへのJSON出力

json::var dataobj;
ofstream is("output.json");
os << dataobj;

制限事項

javascriptの仕様に完全準拠しているわけではありません。

  • NaNを扱えません。
  • Functionを扱えません。
  • オブジェクトのキーは文字列だけです。
  • 数値は内部的にdoubleだけです。

長いサンプル

    // json 文字列からの構築
    json::var num("-1.2345e+3");
    json::var str("'this is string.'");
    json::var arr("[ \"key\", 'str', 'hex', 0xABCD, 0777 ]");
    json::var obj("{ foo : 'bar', 'boo':1.2345e-6, 'arr': [0,1,2,3]}");

    // 参照
    double numval = num;
    string strval = str;
    cout << numval << endl;
    cout << strval << endl;

    numval = arr[3];
    cout << numval << endl;

    strval = (string)obj["foo"];
    cout << strval << endl;

    strval = (string)obj["arr"][2];
    cout << strval << endl;

    // 配列の扱い
    cout << "arr.length() = " << arr.length() << endl;
    arr.push(9.876);
    arr.push("string element");
    arr.push(obj);
    arr.push(arr);
    cout << "arr.length() = " << arr.length() << endl;

    //
    // オブジェクトへ新たなキーを追加
    //
    cout << "obj.exists('new key') = " << (obj.exists("new key")?"true":"false") << endl;
    obj["new key"] = json::var("[0,1,2,3]");
    cout << "obj.exists('new key') = " << (obj.exists("new key")?"true":"false") << endl;

    //
    // std::ostreamへのJSONの書き出し
    //
    ostringstream os;
    os << obj;

    //
    // std::istreamからJSONを読み出し
    //
    istringstream is("{ foo : 'bar', 'boo':1.2345e-6, 'arr': [9,8,7,6] }");
    is >> obj;

    //
    // 別の値(別の型)に書き換え
    //
    num = 4.0;
    cout << num << endl;

    num = "overwrite string";
    cout << num << endl;

    num = obj;
    cout << num["boo"] << endl;

    num["boo"] = "change by reference";
    cout << num["boo"] << endl;

    num["arr"][1] = "change by reference";
    cout << num["arr"][1] << endl;

LICENSE

このソフトウェアは、MIT ライセンスにて、提供します。LICENSE を参照下さい。

This software is released under the MIT License, see LICENSE

Node.js+Sqlite(npm sqlite3)のeachメソッドは中断できない

f:id:takamints:20170829222928p:plain
photo credit: Skakerman 39/52 - Signs via photopin (license)

8月初旬に急遽「C++で画像処理(オブジェクトトラッキング)」ってなプロジェクト(しかもOpenCVを使わないという骨太方針w)のヘルプに駆り出されて、それまでやってた「Node.js+sqlite3」の悩みどころや困りどころを、今やスッカリ忘れそうになっているので、思い出しつつ書いておきます。

[改訂第4版]SQLポケットリファレンス
朝井 淳
技術評論社
売り上げランキング: 95,176

sqlite3のeachメソッドは中断できない

表題と同じですけど大切なことなので2回・・・

ええ、クエリ結果セットから一件ずつレコードを読むための Database#each または、 Statement#each メソッドなんですが、 どちらも公式ドキュメントに「今のところ中断できない」と書いてあって実質的に使えないです。

Database#each

Database#each(sql, [param, …], [callback], [complete])

Runs the SQL query with the specified parameters and calls the callback once for each result row. The function returns the Database object to allow for function chaining. The parameters are the same as the Database#run function, with the following differences:

The signature of the callback is function(err, row) {}. If the result set succeeds but is empty, the callback is never called. In all other cases, the callback is called once for every retrieved row. The order of calls correspond exactly to the order of rows in the result set.

After all row callbacks were called, the completion callback will be called if present. The first argument is an error object, and the second argument is the number of retrieved rows. If you specify only one function, it will be treated as row callback, if you specify two, the first (== second to last) function will be the row callback, the last function will be the completion callback.

If you know that a query only returns a very limited number of rows, it might be more convenient to use Database#all to retrieve all rows at once.

There is currently no way to abort execution.

これ見落としていて、えらい目に遭いました。まさかこんなしれっと書いてあるなんて。

Statement#each(↓)なんて、なぜだかビックリマーク付きですよ。

Statement#each

Statement#each([param, …], [callback], [complete])

Binds parameters, executes the statement and calls the callback for each result row. The function returns the Statement object to allow for function chaining. The parameters are the same as the Statement#run function, with the following differences:

The signature of the callback is function(err, row) {}. If the result set succeeds but is empty, the callback is never called. In all other cases, the callback is called once for every retrieved row. The order of calls correspond exactly to the order of rows in the result set.

After all row callbacks were called, the completion callback will be called if present. The first argument is an error object, and the second argument is the number of retrieved rows. If you specify only one function, it will be treated as row callback, if you specify two, the first (== second to last) function will be the row callback, the last function will be the completion callback.

Like with Statement#run, the statement will not be finalized after executing this function.

If you know that a query only returns a very limited number of rows, it might be more convenient to use Statement#all to retrieve all rows at once.

There is currently no way to abort execution!

結局 all と同じですからー

通常この手のメソッドは、SQLを発行してフェッチした行を順次1行ずつコールバックで通知してくれて、 「もう十分。これ以上必要ない」って時に中断したいわけですが、それが(今のところは)できないってこと。 つまり結局最後まで読みきらないといけないってことで、それなら Database#allStatement#allと同じですよね。

LIMIT使えばいいじゃない / allだけで行けるじゃない?

結局、同じようなことをするためには、SQLでLIMIT句を使って、allメソッドでページングしながら、複数行を繰り返し取ってくるしか手はありませんでした。

結果は大体同じですけど、SQLを複数回発行することになるので、パフォーマンスはちょっと低下すると思います。

LIMIT句をパラメタライズしてStatementを用意しておけば、あまり気にならないとは思いますが。

ま、巨大な結果セットを返さないようにSQLを変更できるなら、それを優先する必要がありますね。

ここぞとコントリビュートしなさいよ

オープンソースなんだから「使えねー」とか文句ばっか言ってないで、フォークして対応してプルリク投げればよいのですが、、、

コレぐらいバカでかいプロジェクトだとちょっと躊躇しますね(笑)

今のところ画像処理方面でアレなんで(言い訳)、またそのうち・・・。

github.com

意外に悩ましい整数部分の四捨五入

f:id:takamints:20170718211752p:plain
photo credit: danmachold calculator via photopin (license)

JavaScriptで整数部分を四捨五入する場合の注意点。桁落ちに気を付けましょうってお話です。

JavaScriptで、小数を整数に四捨五入するにはMath.roundを使いますね。

でもこれ、残念なことに桁数指定ができません。

なので例えば、実数nを小数点以下2桁に丸めたい場合はMath.round(n * 100) / 100 などとしますよね。

毎回掛けたり割ったりするのも邪魔くさいので関数化すればヨロシイですねと、 実数 n を小数点以下 m 桁に四捨五入する関数は、以下のようになりますよっと(Nodeのモジュールとして書いています)。

桁数指定で小数点以下を四捨五入する関数(モジュール)

"use strict";
var assert = require("assert");

/**
 * 小数部の桁数指定で四捨五入。
 *
 * @param {number} n 元の数値
 * @param {number} m 桁数指定。0以上の整数。
 * @returns {number} 結果を返す
 */
function round(n, m) {
    assert(m == Math.round(m));
    assert(m >= 0);
    var r = Math.pow(10, m);
    return Math.round(n * r) / r;
}
module.exports = round;

ここで、 m に負の値を与えれば、整数部分も丸められるように一見思えるのですが、 m の絶対値が大きくなると桁落ちが発生して正しい結果が得られません。いつ桁落ちするかわからないので結局安心して使えません。

桁数指定で整数部分も四捨五入できる関数

てなことで、整数部も正しく四捨五入するために以下のようにしてみました。

"use strict";
var assert = require("assert");

/**
 * 桁数指定で四捨五入。
 *
 * @param {number} n 元の数値
 * @param {number} m 桁数指定。負の値で整数部を四捨五入
 * @returns {number} 結果を返す
 */
function round(n, m) {
    assert(m == Math.round(m));
    if(m < 0) {
        var i = Math.floor(n);
        var R = Math.pow(10, -m);
        var sgn = Math.sign(n);
        var h = sgn * R / 2;
        var mod = sgn * Math.abs(i % R);
        var up = sgn * (sgn >= 0 ? (mod >= h ? R : 0) : (mod < h ? R : 0));
        return  i - mod + up;
    }
    var r = Math.pow(10, m);
    return Math.round(n * r) / r;
}
module.exports = round;

当初「簡単!簡単!」と軽い気持ちで書いたら、 n が負の時に結果がおかしくて、最終的に結構ややこしくなってしまいました。 m < 0 の時、とか、 n の符号による判定が離散的でスッキリしないですねえ。なんかいい方法無いものかな。

言い訳

実は、あまり多くのテストケースを通していないので、ちょっと不安が残っております。

あと、npmには負の桁数指定ができるroundやceil, floorを提供するモジュールはあるようなのですが、 桁落ちに対してどうなっているのか明記されたものが、よくわかりませんでした(英語力の問題かも)。