deeplearn.jsを使ってKaggleのタイタニックをやってみる

唐突ですが、Numeraiをナンピンし無限に損をし続けるにも限界があります。

そう、BTCがもう無いのです。そして心も折れました。

そんな沼底でGBYTEと共に耐え忍ぶ以外の方法を模索し、たどり着いた結論は損切りではなく「データサイエンティストになってNMRを貰おう」でした。

まぁ実際にはそこまで機械学習の沼は浅くないんでしょうが、前から興味があったので良い機会ということで手を出してみました。

ついでに裁量取引ではひどい目にばかり合うので、自動売買のBOTとか作ってみたいという思いもあり。

ということでここから暗号通貨は一切関係無しです。

deeplearn.js

まずは初めの一歩ということで、googleがらみの機械学習ライブラリdeeplearn.jsを少し触ってみました。

deeplearn.jsはブラウザからWebGLを介してGPUを使った機械学習が出来る優れもので、TensorFlowを触るより手軽なんじゃないかと考えて手を出しました。

しかし、機械学習初心者が初めに触るには少しハードルが高く、結局は機械学習の作法について理解するために、最も情報の多いTensorFlowを一から勉強する羽目になりました。

TensorGraphSessionあたりの概念はほぼ同じようなので無駄にはなりませんが、ライブラリでやれることもまだ少ないようなので、これから機械学習に手を出そうという人は素直にTensorFlow触ったほうがいいと思います。

Kaggle

今回は機械学習Hello worldこと、KaggleのTitanicを試してみます。

Kaggleは機械学習のアイデアのコンペをやっているサイトで、チュートリアル的なコンペがいくつかあり、そのうちの1つTitanicをやってみます。

Titanic: Machine Learning from Disaster | Kaggle

タイタニック号の乗員名簿からその生死を予測するというもので、必要なデータは以下にあるtrain.csvtest.csvです。

Titanic: Machine Learning from Disaster | Kaggle

データの前処理

データの前処理は非常に重要です。 むしろこちらが本番なのではないかという気すらします。

初めはこの辺りをあまり考えず、とりあえず数値になっていればいいだろうという考えでやってみましたが、全くうまくいきませんでした。

不要なデータの削除

以下は有用性が低い、もしくは扱いづらいデータです。

  • PassengerId
  • Name
  • Ticket
  • Cabin

使えないデータは列ごと削除します。 Cabinはすごく有用だと思われますが、欠損が多いので除外します。

ただ、マジな人々はTicketからCabinを割り出したり、Nameにある敬称からAgeを割り出したり色々使いどころはあるようです。

欠損値補完

以下はデータに欠損があります。

  • Age
  • Fare
  • Cabin
  • Embarked

平均値や中央値で補完するケースや、欠損のある列を除外するケース、欠損のある行を除外するケース、また機械学習で欠損している部分を推定するケースなど様々なようです。

今回は面倒なのでCabinは捨て、他は平均値で補完します。

標準化

以下はスケールの異なる数値データです。

  • Age
  • SibSp
  • Parch
  • Fare

スケールの異なるデータ間で数値の大小が極端に大きい場合、特定の項目の影響が大きくなりすぎるというようなことが起こるようで、それを避けるために各次元のスケールを合わせます。

一般的にはZスコアが利用されるそうです。

(x - 平均) / 標準偏差

データセットによっては、時系列データなど標準化すべきではないものもあります。

ダミー変数化

以下は数値データではありません。 Pclassは数値ですが、社会階級High、Middle、Lowを表しています。

  • Pclass
  • Sex
  • Embarked

こういったデータを扱う場合は、次元を拡張しmaleの場合[1, 0]、femaleの場合[0, 1]のように別の次元で表現します。

交差検証

過学習が起きていない事、汎化性能が高いことを検証するために、訓練データとテストデータを分け、訓練には訓練データを使い、精度の検証には訓練に使っていないテストデータを使います。

train.csvの前処理が終わったら、訓練データtrainXtrainYとテストデータtestXtestYに分割しておきます。 Xは前処理で作成した各種データの配列で、Yは答えとなるSurvivedの配列です。

deeplearn.jsのインストール

npmでインストールします。

npm install --save deeplearn

必要なものをライブラリからインポートしておきます。

import {
  Array1D,
  Array2D,
  NDArrayMathGPU,
  Scalar,
  Session,
  SGDOptimizer,
  InCPUMemoryShuffledInputProviderBuilder,
  CostReduction,
  Graph,
  Tensor,
  NDArray
} from 'deeplearn';

グラフの構築

Graphを使ってモデルを定義します。

訓練データやテストデータの入れ物になるxtgraph.placeholdershapeのみ決めて定義します。

訓練により最適化されていく変数となるw0b0w1b1graph.variableで初期値と共に定義します。

用意した変数に対してgraph.add(足し算)、graph.matmul(内積)などのメソッドを使って数式を組み、graph.relugraph.sigmoidなどの活性化関数を通して次の層へ出力します。

その他Graphクラスのメソッドについては、以下公式のAPI Referenceに記載されています。

Graph | deeplearn

今回実装したモデルはロジスティック回帰に隠れ層を追加した多層パーセプトロンで、隠れ層の数はいくつか増やしてみたりしたものの結果がいまいちだったので1層だけです。ニューロンの数の増減もあまり良い結果を生みませんでした。

なお、sigmoidに渡すときにreshapeしてあげないとshapeが合わない的なエラーが出て、結構ハマりました。

最後にgraph.meanSquaredCostで損失関数に二乗誤差を指定しています。

const graph: Graph = new Graph();
const x: Tensor = graph.placeholder("x", [12]);
const t: Tensor = graph.placeholder('t', []);

//入力層 - 隠れ層
const w0: Tensor = graph.variable("w0", Array2D.randNormal([12, 12]));
const b0: Tensor = graph.variable("b0", Scalar.randNormal([]));
const h0: Tensor = graph.relu(graph.add(graph.matmul(x, w0), b0));

//隠れ層 - 出力層
const w1: Tensor = graph.variable("w1", Array2D.randNormal([12, 1]));
const b1: Tensor = graph.variable("b1", Scalar.randNormal([]));
const y: Tensor = graph.sigmoid(graph.reshape(graph.add(graph.matmul(h0, w1), b1), []));

const cost: Tensor = graph.meanSquaredCost(y, t);

ちなみにとことんconstで定義している理由は謎です。公式に倣いました。

訓練

訓練(学習)はmath.scopeの中で行います。

訓練データをNDArrayとして構築する際にtrackで追跡することで、scopeの最後で自動的にクリーンアップされるようになります。

InCPUMemoryShuffledInputProviderBuilderで事前にデータをシャッフルします。これ結構重要らしいです。

NUM_BATCHESを増やして訓練を繰り返すほど学習が進んでいきますが、数を倍にしたところであまり結果に差異はありませんでした。

LEARNING_RATEを小さくすると訓練に時間がかかるようになるので、最適なNUM_BATCHESの数も変わりますが、色々試してもあまり顕著に良くなる組み合わせが見つかりませんでした。

オプティマイザはSGD(確率的勾配降下法)しかないようで、それ以外を使いたければ自力で実装するしかなさそうです。

ループの中でsession.trainを実行し、出力される値が徐々に小さくなっていけば訓練が進んでいるということになります。 全く動かない、安定しない、NaNになるというような場合は何かおかしいのでグラフの構築、もしくはデータの前処理から見直したほうが良いです。

const math: NDArrayMathGPU = new NDArrayMathGPU();
const session: Session = new Session(graph, math);

math.scope((keep, track) => {
  const xs: Array1D[] = trainX.map(x => track(Array1D.new(x)));
  const ys: Scalar[] = trainY.map(x => track(Scalar.new(x)));

  const shuffledInputProviderBuilder =
      new InCPUMemoryShuffledInputProviderBuilder([xs, ys]);
  const [xProvider, yProvider] =
      shuffledInputProviderBuilder.getInputProviders();

  const NUM_BATCHES = 500;
  const BATCH_SIZE = xs.length;
  const LEARNING_RATE = 1;
  const optimizer = new SGDOptimizer(LEARNING_RATE);
  for (let i = 0; i < NUM_BATCHES; i++) {
    const costValue = session.train(
        cost,
        [{tensor: x, data: xProvider}, {tensor: t, data: yProvider}],
        BATCH_SIZE, optimizer, CostReduction.MEAN
      );
    console.log("Average cost: " + costValue.get());
  }

推定

事前に作成したテストデータtestXを利用し、session.evalSurvivedを推定し、testYと比較して精度を確認します。

これは訓練のループの中で訓練ごとの精度を確認することも出来ますし、ループを抜けてから最後に実行することも可能です。

for(let i = 0; i < testX.length; i++) {
  const result: NDArray = session.eval(y, [{tensor: x, data: track(Array1D.new(testX[i]))}]);
  let r = result.getValues()[0] > 0.5 ? 1 : 0;
  console.log(r === testY[i]);
}

提出

精度に問題なければtest.csvに同じように前処理を実行し、上記と同じ手順でSurvivedを推定します。

PassengerIdと出力されたSurvivedcsvを作成し、以下ページでアップロードします。

https://www.kaggle.com/c/titanic/submit

結果、Scoreは0.78947でした。

f:id:tadajam:20170922032123p:plain

良いんだか悪いんだか良く分かりませんが、とりあえずベースラインは超えたので良しとします。

そして次は素直にTensorFlow使います。

NEM-libraryを触りながらRxJSを学ぶ

正直言うと今までObservable良く分かんねぇなぁと思いながら使ってました。良く分からないけど、なんかこれオシャレじゃない?的な。

日本語の記事も少ないですし、抽象的な記事が多いのでいまいち理解が進まない。

以下の記事を書く際に参考にしたnem-library-examplesが結構良いサンプルだったので、少し理解が進んだような気がしていましたが、まだ9割ぐらいは雰囲気スクリプターからは抜け出せていませんでした。 tadajam.hateblo.jp

その時参考にしたページ github.com

この辺り詳しい人が誰か教えてくれないかなぁなどと思っていたところ、神が現れました。

NEMの守護神みなりんさん経由で、以下記事を読んでくれたNEM-Libraryの中の人ことAleixさんから連絡があり、もっとイケている書き方を教えてもらいました。 tadajam.hateblo.jp

神々への感謝の気持ちを忘れないために、そしてどこかの誰かの参考になればと記事を残しておきます。

修正前

記事にもともと載せていたソースが以下です。

一つ目のAPIの結果を受けて二つ目のAPIをコールする流れですが、せっかくのRxJSの利点を生かせていません。このどんどんネストしていく感じ、嫌ですね。

一つ目のAPIを呼ぶところでsubscribeして、その結果の配列をforEachでぶん回して二つ目のAPIをそれぞれの要素に対して実行しています。

accountHttp.getMosaicOwnedByAddress(address)
  .subscribe(mosaics => {
    let mosaicHttp: MosaicHttp = new MosaicHttp();
    
    mosaics.filter(x => x.mosaicId.namespaceId !== "nem")
      .forEach(x => {
        mosaicHttp.getMosaicDefinition(x.mosaicId)
          .subscribe(mosaicDefinition => {
            console.log(x.mosaicId, mosaicDefinition);
            console.log(x.mosaicId, x.quantity / (10 ^ mosaicDefinition.properties.divisibility));
          });
      });
  });

修正後

getMosaicOwnedByAddressで流れてくるMosaic配列を、flatMapでバラしてそのまま流します。 流れてきたMosaicfilterをかけてxemを除去します。 xem以外のMosaicを使ってgetMosaicDefinitionを呼び、MosaicMosaicDefinitionを一つのオブジェクトにして流します。 ここをmapで流すとsubscribeの中でさらにsubscribeを呼ぶことになるので、flatMapで流します。

accountHttp.getMosaicOwnedByAddress(address)
    .flatMap(_ => _)
    .filter(mosaic => mosaic.mosaicId.namespaceId !== "nem")
    .flatMap(mosaic => {
      return mosaicHttp.getMosaicDefinition(mosaic.mosaicId)
        .map(mosaicDefinition => <any>{
          mosaicOwnedByTheUser: mosaic,
          mosaicDefinition: mosaicDefinition
        })
    })
    .subscribe(mosaicInformation => {
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicDefinition);
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicOwnedByTheUser.quantity / (10 ^ mosaicInformation.mosaicDefinition.properties.divisibility));
    });

flatMapをうまく使うことで、二つのAPIコールを一つの流れで処理出来ました。

なんだかすごく(3割ぐらい?w)RxJSが分かった気分になってきました。

Thank you Aleix!

NEM Libraryを使ってみる(3) ウォレットの管理

NEM Libraryにはウォレットを管理するクラスも用意されています。

Walletの生成

秘密鍵からアドレスを生成する場合はWalletクラスを継承したSimpleWalletクラスを利用します。

let password: Password = new Password("passwordstr");
let privateKey: string = "privatekeyxxxxxxxxxxxxxxxxxxxxxxxx";

let simpleWallet: SimpleWallet = SimpleWallet.createWithPrivateKey("nextem test", password, privateKey);

生成されたSimpleWalletは以下のようなオブジェクトになり、秘密鍵は暗号化されています。

{
    "name":"nextem test",
    "network":104,"
    address": {
        "value":"ND6VJMWYX7CZRPLYXX566DP2Z6O7ZKYG745EV2AX",
        "networkType":104
    },
    "creationDate":"2017-08-26T11:29:37.729",
    "schema":1,
    "encryptedPrivateKey": {
        "encryptedKey":"encryptedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "iv":"ivxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
}

ちなみに秘密鍵をランダム生成するには、SimpleWalletクラスのcreateメソッドを利用します。

let simpleWallet: SimpleWallet = SimpleWallet.create("random wallet", password);

Walletの利用

公開鍵や秘密鍵を利用するには、openメソッドでAccountクラスを作成して利用します。 秘密鍵はprivateになっているので、秘密鍵を直接操作するのではなく、戻ってくるAccountクラスの各メソッドを利用してトランザクションへの署名や、メッセージの暗号化、復号化を実施します。

let account: Account = simpleWallet.open(password);
account.signTransaction(transaction)

バックアップ

WalletのバックアップはwriteWLTFileメソッドを利用します。 stringが返ってくるので適当に保存します。

let mimetype = 'application/octet-stream';
let url = window.URL.createObjectURL(new Blob([simpleWallet.writeWLTFile()], { 'type': mimetype }));
let a = document.createElement('a');

a.target = '_blank';
a.download = simpleWallet.name + ".wlt";
a.href = url;
a.click();

バックアップからの復元

wltファイルからWalletを復元するには、wltファイルの文字列を抽出した上でreadFromWLTメソッドを利用します。

なお、NanoWalletから出力したwltファイルは形式が異なるようで読み込めませんでした。

let simpleWallet = SimpleWallet.readFromWLT("wltstrxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

このあたり自前で実装するのも面倒なので、WEBサイトなどに導入する際には地味に便利なのではないでしょうか。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM Libraryを使ってみる(2) トランスファートランザクション

次にxemや各種モザイクの送信方法についてです。

xemの送信

xemを送信するには、TransactionHttpクラスのannounceTransactionメソッドを利用します。

送信する署名済トランザクションを作成するには、TransferTransactionクラスのcreateメソッドでトランザクションを作成し、AccountクラスのsignTransactionメソッドで署名します。

let account = Account.createWithPrivateKey("privatexxxxxxxxxxxxx");

let tx = TransferTransaction.create(
    TimeWindow.createWithDeadline(),
    new Address("NCS5BI-MFLIOP-5TMLKM-INCN5C-4PQ5VK-YXK7BB-VGYX"),
    new XEM(0.1),
    EmptyMessage
  );
let signedTransaction: SignedTransaction = account.signTransaction(tx);

let transactionHttp = new TransactionHttp();
transactionHttp.announceTransaction(signedTransaction)
  .subscribe( x => console.log(x));

SUCCESSが返ってきました。

{
  "type":1,
  "code":1,
  "message":"SUCCESS",
  "transactionHash":{
    "data":"28b15df7e2a28e215385ce50ba2e7efb9efe0943db8f585576e071638bd8aa28"
  },
  "innerTransactionHash":{}
}

モザイクの送信

モザイクを送信するには、MosaicHttpクラスのgetMosaicTransferableWithAmountMosaicTransferableオブジェクトを作成し、配列にまとめてからTransferTransactionクラスのcreateWithMosaicsメソッドでトランザクションを作成します。 getMosaicTransferableWithAmountにxemを喰わせるとエラーになったので、個別に配列に追加しました。

ちなみにquantityを1にした場合、divisibilityがいくつであってもしっかりと1送られます。何気にハマるポイントなので気にしなくていいのはありがたいです。

Observable.from([
  {mosaic: new MosaicId("nextem", "nex"), quantity: 1},
  {mosaic: new MosaicId("nextem.ex", "photon"), quantity: 1},
  {mosaic: new MosaicId("nextem.ex", "higgs"), quantity: 1}
]).flatMap(mosaicWithAmount => mosaicHttp.getMosaicTransferableWithAmount(
    mosaicWithAmount.mosaic,
    mosaicWithAmount.quantity
  ))
  .toArray()
  .map(mosaics => {
    mosaics.unshift(new XEM(1));
    return mosaics;
  })
  .map(mosaics => TransferTransaction.createWithMosaics(
      TimeWindow.createWithDeadline(),
      new Address("NCS5BI-MFLIOP-5TMLKM-INCN5C-4PQ5VK-YXK7BB-VGYX"),
      mosaics,
      EmptyMessage
    )
  )
  .map(transaction => account.signTransaction(transaction))
  .flatMap(signedTransaction => transactionHttp.announceTransaction(signedTransaction))
  .subscribe(nemAnnounceResult => {
      console.log(nemAnnounceResult);
  });

残高の確認と送受信。これでもうほとんどのことは出来てしまいます。しかも容易に。

feeも安くなってネームスペースを初めて取ったなんて人も少しずつ出てきているようですし、さらに一歩進めて何か作ってみるというのはいかがでしょうか。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM Libraryを使ってみる(1) 初期設定と各種アカウント情報

以前試してみたNEM-sdkよりも、明らかに使いやすそうなNEM LibraryをAngularで動かしてみます。

TypeScriptできれいに書かれていて、RxJSが使われているのでAngularとも相性が良く、モダンJavaScriptプログラミングのいい教材になると思います。

また、NISからは配列で返ってきて操作しにくいモザイクのpropertiesがちゃんとプロパティになっていたり、ややこしいことで有名なdivisibility周辺も勝手にうまいことやってくれるので、NISの変な癖みたいな部分をきれいに吸収してくれています。

さらにはMultisigAggregateModificationTransactionなどにも対応しているので、機能的にも十分な機能を備えています。

初期設定

npmでパッケージをインストールします。

$ npm install nem-library --save

NEM Libraryはメインネットでもテストネットでも使えるので、app.module.tsあたりでどちらか指定しておきます。

app.module.ts

import { NEMLibrary, NetworkTypes } from "nem-library";

NEMLibrary.bootstrap(NetworkTypes.MAIN_NET);

ちなみに自分の環境(@angular/cli: 1.3.1)では以下のエラーが出ました。

In ambi ent enum declarations member initializer must be constant expression.

これはpackage.jsonにあるdevDependencies部分のtypescriptのバージョンを最新にしてnpm installし直したところ解決しました。 Angular CLIを使って普通にプロジェクト生成した場合のデフォルトのバージョンが2.3.3でしたが、2.4以降が必要なんだと思います。

NISへの接続

NEM LibraryでNISへアクセスする場合、用途に応じてAccountHttpTransactionHttpMosaicHttpなどのHttpEndpointを継承したクラスを利用します。

HttpEndpointを継承したクラスはデフォルトでConnection Poolを使用しているので、引数無しで呼べば接続に成功するまで自動的にNISノードのリストに次々接続していきます。 (NanoWalletもこのような仕組みにすれば、開いたら残高がゼロなんです!!みたいな質問もなくなっていいんじゃないかと思うんですがどうなんでしょう。)

Account情報

AccountHttpを利用するとアカウントの様々な情報を参照出来ます。

xemの残高や公開鍵などの情報を参照するには、getFromAddressメソッドを利用します。 ちなみに一度もトランザクションを発行していないアドレスだと、Not a valid public keyのエラーが出ます。

let accountHttp: AccountHttp = new AccountHttp();
let address: Address = new Address("NDLHY5-KMQTAT-AR7IBR-BF32MA-QWDK73-33VNI2-MD5W");

accountHttp.getFromAddress(address)
  .subscribe(accountInfoWithMetaData => {
    console.log("getFromAddress", accountInfoWithMetaData);
  });

以下のようなAccountInfoWithMetaDataオブジェクトが返ります。

{
  "balance":{
      "balance":32799996,
      "vestedBalance":32796162,
      "unvestedBalance":3834
    },
  "importance":0,
  "publicAccount":{
    "address":{
      "value":"NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W",
      "networkType":104
    },
    "publicKey":"099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db"
  },
  "harvestedBlocks":0,
  "status":"LOCKED",
  "remoteStatus":"INACTIVE",
  "cosignatoryOf":[],
  "cosignatories":[]
}

パブリックキーからAccountInfoWithMetaDataを参照するには、getFromPublicKeyメソッドを利用します、

let publicKey: string = "099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db";

accountHttp.getFromPublicKey(publicKey)
  .subscribe(accountInfoWithMetaData => {
    console.log("getFromPublicKey", accountInfoWithMetaData);
  });

モザイク情報

モザイクの情報を取得するにはMosaicHttpを利用します。

AccountHttpgetMosaicOwnedByAddressで所有モザイクの一覧を取得し、MosaicHttpgetMosaicDefinitionメソッドでそれぞれのモザイクの定義情報を取得します。 xemはgetMosaicDefinitionでエラーになったのでnemネームスペースでフィルターしました。 ※中の人のアドバイスを受けて、RxJSを全力で活用したいい感じのコードに修正しました。

accountHttp.getMosaicOwnedByAddress(address)
    .flatMap(_ => _)
    .filter(mosaic => mosaic.mosaicId.namespaceId !== "nem")
    .flatMap(mosaic => {
      return mosaicHttp.getMosaicDefinition(mosaic.mosaicId)
        .map(mosaicDefinition => <any>{
          mosaicOwnedByTheUser: mosaic,
          mosaicDefinition: mosaicDefinition
        })
    })
    .subscribe(mosaicInformation => {
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicDefinition);
      console.log(mosaicInformation.mosaicDefinition.id, mosaicInformation.mosaicOwnedByTheUser.quantity / (10 ^ mosaicInformation.mosaicDefinition.properties.divisibility));
    });

MosaicDefinitionは以下のようなオブジェクトが返ります。

{
  "creator":{
    "address":{"value":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD","networkType":104},
    "publicKey":"506e25d76aba0bc36dcf28127626a61f1d49ed45352f64fafaa72243c3e9e0ba"
  },
  "id":{"namespaceId":"nextem.ex","name":"photon"},
  "description":"Let there be light.",
  "properties":{
    "initialSupply":1000000,
    "supplyMutable":true,
    "transferable":false,
    "divisibility":0
  },
  "metaId":75
}

アドレスが所有するモザイクの定義と残高を一度に取得するにはAccountOwnedMosaicsServiceを利用します。

let accountOwnedMosaics = new AccountOwnedMosaicsService(new AccountHttp(), new MosaicHttp());
accountOwnedMosaics.fromAddress(address)
  .subscribe(mosaics => {
    console.log("accountOwnedMosaics", mosaics);
  });

以下のようなMosaicTransferableオブジェクト配列が返ります。

[
  {
    "mosaicId":{"namespaceId":"nem","name":"xem"},
    "properties":{
      "initialSupply":8999999999,
      "supplyMutable":false,
      "transferable":true,
      "divisibility":6
    },
    "amount":32.799996
  },
  {
    "mosaicId":{"namespaceId":"nextem.ex","name":"higgs"},
    "properties":{
      "initialSupply":1000000,
      "supplyMutable":true,
      "transferable":true,
      "divisibility":6
    },
    "levy":{
      "type":2,
      "recipient":{
        "value":"NDY4RHUZ3CZOZ53O5HNEXTEM7UF5X3MMDGH4IMAD",
        "networkType":104
      },
      "mosaicId":{"namespaceId":"nem","name":"xem"},
      "fee":10000
    },
    "amount":95.999999
  }
]

トランザクション

トランザクションを参照するには、allTransactionsincomingTransactionsunconfirmedTransactionsなどのメソッドを利用します。 pageSizeを指定しないとデフォルトでは10になります。最小5、最大100です。

accountHttp.allTransactions(address, undefined, undefined, 100)
  .subscribe(transaction => {
    console.log("allTransactions", transaction);
  });

以下のようなTransactionオブジェクト配列が返ります。

[
  {
    "type":257,
    "version":1744830466,
    "timeWindow":{
      "deadline":"2017-08-26T23:49:09",
      "timeStamp":"2017-08-26T21:49:09"
    },
    "signature":"f5d861c62a2abd11a1517911a74123a4bf5a9ec749500bd0f14dcd8059ff3ec5814025969ddf3d79fbd0cbbec81d2f9519bf5c96fcccf76155c67450c86ee20b",
    "signer":{
      "address":{
        "value":"NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W",
        "networkType":104
      },
      "publicKey":"099132a49ed0c15936a464cf6ef43120f01fa88835803593571882feea6161db"
    },
    "transactionInfo":{
      "height":1257790,
      "id":1006473,
      "hash":{
        "data":"39d46a2fcbea6fed97518e0e77fae424c6dc5200bfa1169a76110ba0c84269ce"
      }
    },
    "fee":200000,
    "recipient":{
      "value":"NCS5BIMFLIOP5TMLKMINCN5C4PQ5VKYXK7BBVGYX",
      "networkType":104
    },
    "amount":1000000,
    "message":{
      "payload":""
    },
    "mosaics":[
      {"mosaicId":{"namespaceId":"nem","name":"xem"},"quantity":1000000},
      {"mosaicId":{"namespaceId":"nextem.ex","name":"higgs"},"quantity":1000000},
      {"mosaicId":{"namespaceId":"nextem.ex","name":"photon"},"quantity":1},
      {"mosaicId":{"namespaceId":"nextem","name":"nex"},"quantity":1}
    ]
  }
]

もうこれ完全に暗号通貨の知識いらないですね。


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEMのマルチシグを使ったトラストレスな取引

そもそもの仕様を理解していなかったようで、コメントで頂いた指摘を受け修正しました。 (2of2のマルチシグ ⇒ 2of3のマルチシグ)

やっぱりしっかり検証してないとダメですね。(まだしてない


今回は、前々から色々考えて実験したりしていたことと、夜中に突然目覚めて関連することで思いついたことがあったので併せて残しておきます。

真夜中に目覚めて初めに考えたことがマルチシグの活用だなんて、我ながら消耗していますね。

ちなみに、結論から言っておくとあんまり価値のない話なんですが、考えていると結構面白かったので。

まず、Catapultのホワイトペーパーに以下のようなことが実現出来ると書いてありました。

ブロックチェーン上でアセットを取引するための、組み込み型の預託(エスクロー)サービス。トランザクション型コントラクト。

これがあればDEXも簡単に実装出来そうですし、待ち遠しい限りです。

取引所があるだけで、今までただ投げ合っていた純粋に無価値なトークンが、ほぼ無価値なトークンに格上げされます。 まぁほぼ無価値なことに変わりはありませんが、ゼロかゼロでないかは大きな差ですね。

ただ、実際にCatapultがNEMにまでフィードバックされるのはいつでしょう?夏なのか、秋なのか、冬なのか、または。。 まぁいつになるのか分かりませんので、それまでのつなぎのサービスがあっても良いと思います。

ということで、Mosaic交換の肝になる資金のエスクローについて、現状の標準機能でどこまで実装できるのかについて考えてみました。

資金のエスクロー

NEMの標準機能であるマルチシグアドレスを使って実現します。

以下をそれぞれ交換するとします。

  • アドレスA nem:xem
  • アドレスB nextem:nex

資金の移動用に以下アドレスを利用します。

  • アドレスC
  • アドレスD
  • アドレスE ダミーアドレス

リファンド用トランザクションの作成

以下のトランザクションをtimestampを1時間後など未来の時間にして作成します。

  • 「アドレスCからアドレスAへnem:xemを送金」をラップしたマルチシグトランザクションにアドレスBの秘密鍵を使って署名。
  • 「アドレスCからアドレスBへnextem:nexを送金」をラップしたマルチシグトランザクションにアドレスAの秘密鍵を使って署名。

それぞれの署名済みトランザクションをブロードキャストせずにアドレスAとアドレスBで交換します。 交換が終わったら、それぞれ署名に問題ないか事前に検証しておきます。

エスクロー用アドレスの作成

アドレスCを2of2マルチシグアドレス(連署人がアドレスAとアドレスB)に変換します。

以下の指摘の通り、連署人を勝手に外せる2of2ではダメみたいです。

id:mizunashi_rin 2017-06-26 13:41:50 NEMのMultisignature aggregate modification transactionの仕様で、N-of-Nのmultisig構成を組んでいる場合は、N-1の署名(つまりIsserのみ)で連署名者を削除できます。 この様な仕様を盛り込んだ形で、再度事例を具現化した記事を読みたいです♪ feeに関しては、1ヶ月後には1/20になっていると思われます。

誰も秘密鍵を知らないことが既知のダミーアドレスEを連署人に加えた2of3にすることで、連署人の追加削除を禁止します。

アドレスCを2of3マルチシグアドレス(連署人がアドレスAとアドレスBとダミーアドレスE)に変換します。

アドレスCへそれぞれ交換したいMosaicと必要なfeeを入金します。

2of3なのでもう資金はどちらかが勝手に動かすことは出来ません。

また、片方が入金しなかったような場合も、交換した署名済みのトランザクションがあるので、1時間後には自分の資金をリファンドすることが出来ます。

交換処理用アドレスの作成

BTCのようにひとつのトランザクションで同時に複数のアドレス宛に送金出来れば簡単なのですが、NEMではそれぞれへの送金を一つのトランザクションにまとめることが出来ないので、もうワンクッション必要になります。

複数アドレス宛には送れませんが、複数Mosaicを一つのアドレスに送ることは可能なので、それを利用します。

以下のトランザクションを作成します。

  • 「アドレスDからアドレスAへnextem:nexを送金」をラップしたマルチシグトランザクションにアドレスBの秘密鍵を使って署名。
  • 「アドレスDからアドレスBへnem:xemを送金」をラップしたマルチシグトランザクションにアドレスAの秘密鍵を使って署名。

それぞれの署名済みトランザクションをブロードキャストせずにアドレスAとアドレスBで交換します。 交換が終わったら、それぞれ署名に問題ないか事前に検証しておきます。

アドレスDを2of3マルチシグアドレス(連署人がアドレスAとアドレスBとダミーアドレスE)に変換します。

アドレスDへアドレスCからそれぞれのMosaicを同時に送金します。

ブロードキャストせずに交換した署名済みのトランザクションを使って、交換を完了させます。

トラストレス!

問題点と結論

NEMの標準機能だけを使ってDEXを作ろうと思うと、上記署名の交換などをmessageでやり取りすることになると思うので、正直現実的ではないレベルでfeeが必要になります。

messageをシグナリングサーバー代わりにして、WebRTCとかで接続すればP2Pでやり取り出来るのかもしれませんが、なんだか複雑になりそうです。

また、マルチシグアドレスを使うので、これまたfeeが高くなります。2of2マルチシグアドレスを二つ作成するだけで56xemかかり、なんとこれ今千円以上なんですね。。

やはりこれも現実的ではないです。

ということで結論ですが、下手にDecentralizedとか言わずに、素直にWEBサービスを立ち上げればいいんじゃないか、とw

今なんとなく想定しているのは、資金のロックすらせず、取引が成立したら送金トランザクションの署名を集めて、揃ったら残高だけ確認して問題なければブロードキャストするというような、ゆるーいサービスです。

需要あるかな?


※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD

NEM-sdkを使ってみる WebSocket編

少しバタバタしていたので間が空いてしまいましたが、引き続き以下のNEM-sdkを使ってみます。

github.com

今回はsdkを利用してWebSocketのAPIと接続してみます。

コネクターオブジェクトの作成

まずendpointaddressを渡してconnectorオブジェクトを作成します。

let endpoint = nem.model.objects.create("endpoint")
  (nem.model.nodes.defaultMainnet, nem.model.nodes.websocketPort);
let address = "NDLHY5KMQTATAR7IBRBF32MAQWDK7333VNI2MD5W";
let connector = nem.com.websockets.connector.create(endpoint, address);

NISへの接続

次にconnectorオブジェクトのconnectを呼んでNISへ接続し、返されたPromiseのコールバック関数でチャネルの購読処理や、リクエストを行います。

subscribe

チャネルの購読処理にはnem.com.websockets.subscribeを利用します。

  • errors

 文字通りなんだとは思いますが、今のところ何か流れてきたことがありません。

  • chain.height

 現在のブロック高を返します。

  • chain.blocks

 最新のブロックの情報と含まれるトランザクションの配列を返します。

  • account.data

 アカウントの残高やパブリックキーなどの情報を返します。

  • account.transactions.recent

 該当アドレスの最新25個の承認済トランザクション情報を返します。

  • account.transactions.unconfirmed

 該当アドレスに関する未承認トランザクションを返します。

  • account.transactions.confirmed

 未承認トランザクションが承認されたタイミングで、そのトランザクションを返します。

  • account.mosaics.definitions

 該当アドレスが所有するモザイクの定義を返します。xemも含まれます。配列ではなくモザイクごとに別々にオブジェクトを返します。

  • account.mosaics.owned

 該当アドレスのモザイク残高を返します。xemも含まれます。配列ではなくモザイクごとに別々にオブジェクトを返します。

  • account.namespaces.owned

 該当アドレスがオーナーのnamespaceを返します。

requests

こちらからデータを要求する場合は、nem.com.websockets.requestsを利用します。

  • account.data
  • account.transactions.recent
  • account.mosaics.definitions
  • account.mosaics.owned
  • account.namespaces.owned

それぞれ該当のチャネルを購読していれば、そこにデータが流れてきます。

なお、account.transactions.recentをリクエストすると、承認済トランザクションと共に未承認トランザクションも返ってきます。逆に、初回起動時にaccount.transactions.recentをリクエストしていない場合、account.transactions.unconfirmedを購読していても、すでにブロードキャスト済で未承認なトランザクションを把握出来ません。

コード

そのまま書くと関数名がいちいち長くて見づらいですが、コードは以下のようになります。

connector.connect().then(() => {
  console.log("Connected");

  nem.com.websockets.subscribe.errors(connector, res => console.log("errors", res));
  nem.com.websockets.subscribe.chain.blocks(connector, res => console.log("blocks", res));
  nem.com.websockets.subscribe.chain.height(connector, res => console.log("height", res));
  nem.com.websockets.subscribe.account.data(connector, res => console.log("data", res));
  nem.com.websockets.subscribe.account.transactions.recent(connector,  res => console.log("recent", res));
  nem.com.websockets.subscribe.account.transactions.unconfirmed(connector, res => console.log("unconfirmed", res));
  nem.com.websockets.subscribe.account.transactions.confirmed(connector, res => console.log("confirmed", res));
  nem.com.websockets.subscribe.account.mosaics.definitions(connector, res => console.log("definitions", res));
  nem.com.websockets.subscribe.account.mosaics.owned(connector, res => console.log("owned", res));
  nem.com.websockets.subscribe.account.namespaces.owned(connector, res => console.log("namespaces", res));

  nem.com.websockets.requests.account.data(connector);
  nem.com.websockets.requests.account.transactions.recent(connector);

  nem.com.websockets.requests.account.mosaics.definitions(connector);
  nem.com.websockets.requests.account.mosaics.owned(connector);
  nem.com.websockets.requests.account.namespaces.owned(connector);

}, err => console.log("errorMessage", err));

実行時の流れ

まず接続に成功するとConnectedが出力され、リクエストしたdatarecentdefinitionsowned、存在すればnamespacesが返り、ブロックが進むごとにheightblocksが返ります。

ちなみに接続に失敗すると10回繰り返してその後エラーが出力されます。

送金や着金があった場合、まずunconfirmedが返ります。 この時点ではnem.com.websockets.requests.account.dataで最新データを要求してもまだ残高は変わっていないので、未承認トランザクションを含めるとどうなるかはこのunconfirmedの結果を反映してあげる必要があるようです。

ブロックが進むとheightblocksが返り、未承認のトランザクションが承認に変わるのでconfirmedが返ります。

承認されると自分の残高が変わるので最新のdataが返ります。ゼロになった場合は残高ゼロで値が返ります。

モザイク

xem以外のモザイクに動きがあった場合はdefinitionsownedが所有モザイクの数だけそれぞれ返りますが、ゼロになった場合はゼロが返るのではなく、該当モザイクのオブジェクトは何もかえってきません。

手持ちのモザイクの一つを送信してゼロになった場合、モザイクに動きがあったので残された他のモザイクについてdefinitionsownedは返りますが、該当のモザイクについては何も返らないという事になります。

返ってきたオブジェクトを次々と配列に入れていく作りにした場合、ゼロになったタイミングを把握出来ないのでいまいち使いにくいです。NanoWalletの実装もそうなっていて、送金後ゼロになった場合も、残高は更新されません。(少なくとも1.2.12ではそうでした)

何故配列で返す仕様にしなかったんだろう??

とりあえず、承認されたトランザクションを元に自力で計算するか、confirmedのところで一旦配列をリセットし、definitionsownedを強制的にリクエストして再度配列を作り直すなど工夫が必要そうです。


これで残高、未承認トランザクションなど、リアルタイムでデータを更新可能なサービスが作れるようになりました。

今度こそチャーハンを作る準備は整った。かな?

※筆者のモチベーション向上のため、以下NEMアドレスへxemなりシットトークンなりの寄付を受け付けています。

NDY4RH-UZ3CZO-Z53O5H-NEXTEM-7UF5X3-MMDGH4-IMAD