銀の弾丸

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

DynamoDBをSQLで操作するNodeモジュール

Amazon DynamoDBSQL的な記述言語(SQL-ish)で操作できるNodeモジュールのご紹介。

f:id:takamints:20180206230625p:plain

目次

www.npmjs.com



概要

このモジュールからはDynamoDBのテーブルをSQL的な文(SQL-ish)で操作できるステートメントを提供しています。

ステートメントを生成する関数は以下の4つ:

それぞれ、引数にSQL(によく似た)文を与えてステートメントを生成します。 各ステートメントは後からパラメータを与えて実行可能(※ただしDeleteItemStatementは現在パラメタライズが出来ません)。

このほか、ScanとQueryの結果の中の Itemsを(DynamoDBのマップ型の配列から)普通のオブジェクトの配列に変換する DynamoDbResultSet クラスgetItemsメソッドが使えます。

また、v0.9.6では、 PutItemStatement が返す DynamoDBPutItemStatementクラスsetValuesメソッドで、新たなVALUESを設定してステートメントを実行できます。

以下にScanとQueryの簡単なサンプルコードを掲載してます。 他のステートメントのサンプルやAPIの詳細はAPIリファレンスを参照してくださいね。

SQL-ish Statement クラスの使用例

実際に動作させるには、DynamoDBにstarsというテーブルがあり、HASHキーがString型のmainStar、RANGEキーがNumber型のorbitOrderとなっている必要があります。

"use strict";
const awsNodeUtil = require("aws-node-util");
const ScanStatement = awsNodeUtil.dynamodb.ScanStatement;
const QueryStatement = awsNodeUtil.dynamodb.QueryStatement;
const ResultSet = awsNodeUtil.dynamodb.ResultSet;

// Connect (change each value for your account)
awsNodeUtil.dynamodb.connect(
//    { accessKeyId: 'AKID', secretAccessKey: 'SECRET', region: 'us-west-2' }
);

// Prepare 'Scan' statement
var scanStatement = ScanStatement(
        "FROM stars WHERE name=:name");

// Prepare 'Query' statement
var queryStatement = QueryStatement(
        "SELECT mainStar, orbitOrder, name " +
        "FROM stars " +
        "WHERE mainStar=:mainStar");

// Run the statements
ScanStatement("FROM stars").run({}, (err, resp) => {
    console.log("-----------------------");
    console.log("SCAN all items of stars");
    console.log("-----------------------");
    printResult(err, resp);
    scanStatement.run({ ":name": "EARTH" }, (err, resp) => {
        console.log("--------------");
        console.log("SCAN the EARTH");
        console.log("--------------");
        printResult(err, resp);

        queryStatement.run({ ":mainStar": "EARTH" }, (err, resp) => {
            console.log("------------------------------");
            console.log("QUERY child stars of the EARTH");
            console.log("------------------------------");
            printResult(err, resp);
        });
    });
});

// Handler to print result of scan / query
function printResult(err, result) {
    if(err) {
        console.error("Error:", err.stack);
    } else {
        ResultSet.printScanResult(result);
    }
}

実行時の出力例:

$ node sample/sqlish-sample.js
-----------------------
SCAN all items of stars
-----------------------
Count: 10
ROWNUM diameter rotation role      mass      gravity density escapeVelocity name    orbitOrder mainStar
     1     3475    655.7 satellite    0.0073     1.6    3340            2.4 MOON             1 EARTH
     2     4879   1407.6 planet       0.33       3.7    5427            4.3 MERCURY          1 SUN
     3    12104  -5832.0 planet       4.87       8.9    5243           10.4 VENUS            2 SUN
     4    12756     23.9 planet       5.97       9.8    5514           11.2 EARTH            3 SUN
     5     6792     24.6 planet       0.642      3.7    3933            5.0 MARS             4 SUN
     6   142984      9.9 planet    1898.0       23.1    1326           59.5 JUPITER          5 SUN
     7   120536     10.7 planet     568.0        9.0     687           35.5 SATURN           6 SUN
     8    51118    -17.2 planet      86.8        8.7    1271           21.3 URANUS           7 SUN
     9    49528     16.1 planet     102.0       11.0    1638           23.5 NEPTUNE          8 SUN
    10     2370   -153.3 planet       0.0146     0.7    2095            1.3 PLUTO            9 SUN
ScannedCount: 10
--------------
SCAN the EARTH
--------------
Count: 10
ROWNUM diameter rotation role   mass gravity density escapeVelocity name  orbitOrder mainStar
     1    12756     23.9 planet 5.97     9.8    5514           11.2 EARTH          3 SUN
ScannedCount: 10
------------------------------
QUERY child stars of the EARTH
------------------------------
Count: 1
ROWNUM name orbitOrder mainStar
     1 MOON          1 EARTH
ScannedCount: 1

SQL-ishの構文

各Statementの構文を以下にざっくり説明します。 []は省略可能を意味します。<> は後述します。

/* Scan */
[SELECT <projection-expression>]
FROM <table-name>
[WHERE <filter-expression>]
[LIMIT <limit>]

/*Query*/
[SELECT <projection-expression>]
FROM <table-name>
WHERE <key-condition-expression>
[FILTER <filter-expression>]
[LIMIT <limit>]

/*PutItem*/
INSERT INTO <table-name> ( <attribute-list> )
VALUES ( <value-list> )
[WHERE <key-condition-expression>]

/*DeleteItem*/
DELETE FROM <table-name>
[WHERE <key-condition-expression>]

SELECT句

SELECT句には<projection-expression>にカンマ区切りで選択する属性名を指定します。 通常のSQLと違ってSELECT句は省略可能です。省略時は全属性が選択されます。 (「属性」は一般的なDBでは列、またはカラムに相当します)

FROM句

FROM句は必須です。 <table-name>に検索を行うテーブル名を指定します。JOINなどは出来ません。

WHERE句

WHERE句はScanとその他のステートメントでは意味が違います。

ScanStatementでは、フィルター条件式(<filter-expression>)を指定します。これは省略可能です。省略すると全件取得。 フィルタ条件式にはキー属性を記述できません。これが必要な場合はQueryを使用します。

他のステートメントではキーの条件式(<key-condition-expression>)を指定します。 QueryStatementでは必ず記述しなければなりません。 キーの条件式には、キー以外の属性を記述できません。 QueryStatementでキー以外の属性で絞り込みたい時はFILTER句を使用します。

その他、キーの条件式には、以下のような制限もあります。これらは全てDynamoDBのKeyConditionExpressionの制限です。

  • パーティションキーの条件は必須で、 = で一致させなくてはなりません。
  • ソートキーの条件をANDで追加できます。この条件式で使用できる比較演算子=, <, <=, >, >=, BETWEEN ~ ANDだけです。INは使用できません。関数はbegins_withだけ使用できます。
  • 詳細はQuery - Amazon DynamoDB(英文)に書いてあります。

FILTER句

FILTER句は、QueryStatementにフィルタを設定するために記述します。 省略可能。省略時はフィルタはかかりません。

ScanStatementにFILTER句は指定できません(WHERE句に指定)。

LIMIT句

LIMIT句は<limit>にScanする件数(数値)を指定します。

省略可能ですが注意が必要。

LIMIT句を省略するとScanStatementではWHERE句(フィルタ)の指定に関わらず、全件をスキャンすることになります。 QueryStatementでは必ずパーティションキーの条件が入っていますのである程度件数は絞られます。 いずれにせよ項目数の多いテーブルでLIMIT句を省略すると処理速度や料金に影響しますので注意して下さい。 これはDynamoDBの仕様です。

VALUES句

PutItemStatementのVALUES句は、( <attribute-list> ) VALUES ( <attribute-list> )という形式で、 追加・上書きする項目の属性(RDBでのカラム名に相当)と値をそれぞれカンマ区切りで並べたものです。 属性と値の数は一致しなければなりません。 各値の型は表記から自動的に判断されます。

プレースホルダーについて

DynamoDBを使う上でちょっと邪魔くさい「名前のプレースホルダー」、「値のプレースホルダー」について気にする必要はありません。 本モジュール内で構文解析時に自動的に識別して変換します。

ステートメントのパラメタライズ

上の例に示しているように、パラメータ化する箇所には半角コロン:で始まる識別子を直接記述しておけば、実行時にパラメータを与えて処理できるようになります。

DynamoDBについての基本事項

DynamoDBはNoSQL

DynamoDBはAWSAmazon Web Service ― のNoSQL型データベース。 リレーショナルではありませんので複数テーブルの突き合わせ(JOIN)とかは出来ません。

DynamoDBのScanとQueryの違いについて

ScanもQueryもDynamoDBのテーブルから項目を読み込む機能ですが、読み込み方に違いがあります。

Scanはキーによる絞り込みが出来なくて、項目の並び順に順次読み込み(=スキャン)する感じ。 Queryはスキャンの前にキーの条件で絞り込めます。

どちらもスキャンしたあとに「フィルター」をかけられ、スキャンする項目数を指定できます。 また、続きから読むという操作も可能です。

あとがき

DQLというPython製ですが似たコンセプトのモジュールがあるらしいですね。 使ったことがないのでよくわからないですが、シェルから実行できる対話式のクライアントという感じでしょうか。

当npmをリリースした後「似たようなのって既にあるんじゃないの?」って検索して知ったのでワタシは無罪。 しかし、こちらが後発なのは明らかなので今後構文的にDQLに寄せていくのもありかなと思っています。BNFもドキュメントで規定されているし。

package-lock.jsonの潜在的セキュリティ脆弱性を解消しました

f:id:takamints:20180112204146p:plain
photo credit: wuestenigel blue padlock via photopin (license)

昨年12月末、GitHubに置いてる自作npmリポジトリに「潜在的なセキュリティの脆弱性がありますよ」ってメッセージが表示されるようになって、 「公開しているnpmのリポジトリに、こんなの表示されたらかなわんなあ」と思いながらも、 なんか怖いし対処法も分からんし、2週間ぐらいは見て見ぬふりしていましたが、この度正しく対処してメデタク解消致しました。

結果的には大したことはしていませんが記録として書いておきます。

関連する最新記事(↓): なんとワンクリックで対処できるようになってます。 takamints.hatenablog.jp

潜在的セキュリティ脆弱性があるらしい

何やら怪しげなメッセージは、コチラ(↓)です。

f:id:takamints:20180110111240p:plain

”potential security vulnerabilities”が「潜在的セキュリティ脆弱性」。

文章部分を、ちゃんと翻訳すると以下のようになりますね。

We found potential security vulnerabilities in your dependencies. Some of the dependencies defined in your package-lock.json have known security vulnerabilities and should be updated. Only the owner of this repository can see this message.


「わたしたちは、あなたの依存パッケージに潜在的なセキュリティ脆弱性の可能性を見つけました。 あなたの package-lock.json 内の依存パッケージのいくつかは、既知のセキュリティ脆弱性があり、これらは更新されるべきです。 リポジトリのオーナーだけがこのメッセージを見ることができます。

最後の一文でホッとしましたが、どうすりゃいいかはわからない。 とりあえず「Review Vulnerable dependencies」ボタンを押せば、以下(↓)の詳細が表示されます。

f:id:takamints:20180110113541p:plain

このページ、昨年末にGitHubに追加された機能ですね。「早速動いているんだな」と当たり前のことに感心しました。

それはさておき、どうやら marked0.3.6脆弱性があるようです。 そして、小さなドロップダウンを開くと、修正された 0.3.9 以降に更新すればいいよと言ってるようだ。

ただし自作パッケージではmarkedを使用していません。依存パッケージのどれかが依存しているのでしょう。

package-lock.jsonを編集するのはよろしくないかも

最初は単純に package-lock.json に記述されてる marked のバージョンを 0.3.9に書き換えればよいと思いましたが、そうでもなさそう。 セマンティックバージョニングを厳格に適用するなら、以下の理由によって正しく動作する保証がありません。

  • marked に依存しているパッケージは marked@0.3.9 で動かされた実績がありません(パッチレベルのバージョンアップですので実際には動くかもしれませんが、それでも確証はありません)。
  • markedのメジャーバージョンは0(=正式リリースされていない状態)なので互換性が保証されない可能性があります。
  • package-lock.json は古いバージョンに固定したい(勝手に新しいバージョンが使われないようにする)時に使うものだと思ってます(あってますかね?)ので、新しいバージョンに固定するのは気持ち悪い。

ということから、直接 marked に依存しているパッケージを更新し、marked@0.3.9 以上に依存するのが安全ぽいという結論に。

どいつが依存しているんだ?

しかし、package-lock.json を開いても、どのパッケージがmarkedに依存しているかという情報はないのですね。

npmの依存関係は npm ls で全部表示できるのですが、恥ずかしながらこの時スッカリ忘れていまして、findとgrepで node_modules以下にインストールされた全パッケージのpackage.jsonからmarkedをゴリゴリ検索しちゃいました。これ結構時間がかかりますので良い子は真似しない。npm ls はnode_module以下を見ずに依存ツリーを表示してくれるのでこちらが標準。

$ find . -name 'package.json' -exec egrep -H '^\s*"marked"' {} \;
./express/package.json:    "marked": "0.3.6",
./jsdoc/package.json:    "marked": "~0.3.6",
./marked/package.json:    "marked": "./bin/marked"

はい出ました。expressjsdocでした(最後の行はmarked自身)。

あとは更新してプッシュ

どちらも、npmで確認すると新バージョンが出ておりまして、それぞれ修正済みのmarkedへ依存していました。 そこで、この2つのパッケージを最新版に更新し、package-lock.json も更新し、 GitHub へアップすれば一件落着。

Push後すぐにはメッセージは消えませんでしたが、少なくとも1時間後には消えていました。

github.com

まとめ

  • ちょっと邪魔くさかったけど、定期的に npm-check-updates してればこういう目に合わなくてよいのかも知れないなと。
  • npm 5で追加されたpackage-lock.jsonについて、きちんと理解する必要があると痛感しました。

www.npmjs.com

WindowsでSwift使う(iPhone開発とは言ってない)

f:id:takamints:20171227204509p:plain

詳解 Swift 第4版
詳解 Swift 第4版
posted with amazlet at 17.12.27
SBクリエイティブ (2017-12-26)
売り上げランキング: 2,188

Swift For Windowsをインストールすればヨシ。

デフォルトで C:\Swift にインストールされる。

GUIから使うにはこのままで良いのだけれど、できればCLIから使いたい。

コマンドプロンプトPowerShellから使うには以下の4つにPATHを通す。

C:\Swift\mingw64\bin;
C:\Swift\wxWidgets-3.0.3\lib\gcc510TDM_x64_dll;
C:\Swift\usr\lib\swift\mingw;
C:\Swift\usr\bin;

これで難なくコンパイルできるし実行モジュールもダブルクリックで実行可能。GitBashからもOKですね。

しかしESP-IDF用にインストールしたMSYS2からは使えなかった(なんでか知らん)。 そこで、上記とほぼおなじことだけど、同MSYS2の .bash_profile に以下の4行を追加する。

PATH="${PATH}:/c/Swift/mingw64/bin"
PATH="${PATH}:/c/Swift/wxWidgets-3.0.3/lib/gcc510TDM_x64_dll"
PATH="${PATH}:/c/Swift/usr/lib/swift/mingw"
PATH="${PATH}:/c/Swift/usr/

これでめでたくいたるところでSwiftをコンパイルできて実行可能。おめでたい。

まあ、iPhoneアプリは開発できないけど、言語は習得できますね。

参考リンク:

コンソールから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