Node-REDにNatural Language Classifierノードを追加する

Node-RED Starterは非常に便利ですが、Watsonに関しては最新のAPIに追随しているとは言いがたい状況です。たとえば、現時点ではNatural Language Classifier (NLC)のノード定義がありません。そこで、NLCのノードを自作して追加してみました。

# 前提

* Bluemixアカウント(IBM-ID)の取得
* Cloud Foundry CLI (cf)のインストール
* Curlのインストール
* jqのインストール

# サンプル分類器の作成

あとで使うためのサンプル分類器を作ります。
ここでは、IBM Watsonチームが提供している、天気に関するサンプル(weather_data_train.csv)を借用して、天気に関する質問文の分類器を作成します。

```bash
#
# 分類器に学習させるサンプルCSVを取得
#

# IBM Watsonチームのサンプルプロジェクトをクローン
$ git clone https://github.com/watson-developer-cloud/natural-language-classifier-nodejs.git

# ファイルの中身を確認
$ cat natural-language-classifier-nodejs/training/weather_data_train.csv

#
# NLCサービスを作成
#

# サービス、およびサービスにアクセスするためのcredential情報を作成
$ cf create-service natural_language_classifier standard weather-classifier
$ cf create-service-key weather-classifier credentials

# Credential情報の中身を確認し、コマンドの結果の"username"と"password"を以下で使用
$ cf service-key weather-classifier credentials

#
# 天気に関する質問文の分類器を作成
#

# 分類器を作成(curlが出力する"classifier_id"はあとで分類器を特定するために使用)
$ curl -s -u "{usernamne}:{password}" -X POST -F training_data=@natural-language-classifier-nodejs/training/weather_data_train.csv -F training_metadata="{\"language\":\"en\",\"name\":\"weather\"}" https://gateway.watsonplatform.net/natural-language-classifier/api/v1/classifiers

# 学習状況を確認
# このコマンドが出力するjsonにある"status"が"Training"から"Available"に変わった時点で学習が終了
$ curl -s -u "{usernamne}:{password}" https://gateway.watsonplatform.net/natural-language-classifier/api/v1/classifiers/{classifier_id}

# 分類器を使ってみる("class_name"と"confidence"を含むjsonが出力されればOK)
$ curl -s -u "{usernamne}:{password}" -X POST -H "Content-Type:application/json" -d "{\"text\":\"Is it cloudy today?\"}" https://gateway.watsonplatform.net/natural-language-classifier/api/v1/classifiers/{classifier_id}/classify | jq '.classes[0]'
```

# Node-REDアプリケーションの作成

BluemixコンソールのCATALOGからNode-RED Starterアプリケーションを作成します。
CATALOGでは次のアイコン(Node-RED Starter / Community BETA)です。

![noderednlc-starter-icon.png](https://qiita-image-store.s3.amazonaws.com/0/21963/ce34016d-49f1-6162-dbb8-618c5996b60a.png)

このアイコンを選択するとアプリケーション設定画面に移りますので、プロジェクトの名前(この例では"weatherqclass")のみ入力してアプリケーションを作成します。

![noderednlc-create-app.png](https://qiita-image-store.s3.amazonaws.com/0/21963/38412966-abee-07fd-112a-c6eb76ad54f2.png)

Node-RED Starterでは自動的にアプリケーションのステージングが開始されます。画面には「Your application is staging ...」と出ますので、暫く待っていると、「Your app is running」に変わります。
それでは、ブラウザでhttp://weatherqclass.mybluemix.net/に接続してみましょう。

![noderednlc-initial-app.png](https://qiita-image-store.s3.amazonaws.com/0/21963/5c703dba-b2b6-ff45-a4b3-af7081fb454f.png)

右の方にある「Goto your Node-RED flow editor」をクリックすると、フローエディタが開きます。
フローエディタの左側にノード一覧があり、この最下部にWatsonサービス群のノードがあります。

![noderednlc-watson-nodes.png](https://qiita-image-store.s3.amazonaws.com/0/21963/e2bd8ac0-0566-f3bc-2df3-3565bbe3b106.png)

NLCはありません。今回のゴールはここにNLCノードを作成し、フローで使ってみることです。

# Node-REDフローエディタへのノードの追加

## Node-REDアプリケーションのソースファイルのダウンロード

まず、フローエディタのソースをローカル開発環境にダウンロードします。具体的には、さきほど「Your app is running」と表示された画面(左側のメニューでは「Start Coding」)に戻り、「Download Starter Code」と記されたボタンをクリックします。

![noderednlc-download-code.png](https://qiita-image-store.s3.amazonaws.com/0/21963/f8fd3852-34af-967c-f4fe-1a7a6628ca56.png)

すると、ブラウザのダウンロードフォルダにzipがダウンロードされますので、これを適当なディレクトリに展開します。

```bash
$ mkdir weatherqclass
$ cd weatherqclass
$ unzip ~/Downloads/weatherqclass.zip 
```

## ノードを追加する手順

ノードを追加する手順は以下の通りです。

1. ノードの外形的表現(nodes/nlc.html)
2. ノードの振る舞い表現(nodes/nlc.js)
3. ノードのアイコン(nodes/icons/nlc.png)
4. パッケージへの追加(package.json)

なお、すべてを変更したソースのサンプルが[github上](https://github.com/blackaplysia/weatherqclass.git)に置いてあります。

## 1. ノードの外形的表現(nodes/nlc.html)

ノードの外形的表現は、nodesディレクトリ以下にhtmlで作成します。このhtmlは大きく3つのscriptから構成します。

### (1) 設定項目と設定ダイアログの名称、アイコン、プレイスホルダ等

```html:nodes/nlc.html
<script type="text/x-red" data-template-name="natural-language-classifier">
  <div class="form-row">
    <label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
    <input type="text" id="node-input-name" placeholder="Name">
  </div>
  <div class="form-row">
    <label for="node-input-username"><i class="fa fa-user"></i>Username</label>
    <input type="text" id="node-input-username" placeholder="NLC Username">
  </div>
  <div class="form-row">
    <label for="node-input-password"><i class="fa fa-key"></i>Password</label>
    <input type="password" id="node-input-password" placeholder="NLC Password">
  </div>
  <div class="form-row">
    <label for="node-input-classifierid"><i class="fa fa-tag"></i>Classifier ID</label>
    <input type="text" id="node-input-classifierid" placeholder="NLC Classifier ID">
  </div>
</script>
```

### (2) ヘルプの記述

```html:nodes/nlc.html
<script type="text/x-red" data-help-name="natural-language-classifier">
  <p>The natural language indentifier service.</p>
</script>
```

### (3) エディタ上のノードの基本的な設定

```html:nodes/nlc.html
<script type="text/javascript">
  (function() {
    RED.nodes.registerType('natural-language-classifier', {
      category: 'IBM_Watson',
      defaults: {
        name: { value: "" },
        username: { value: "" },
        password: { value: "" },
        classifierid: { value: "", required: true }
      },
      color: "#FFAAAA",
      inputs: 1,
      outputs: 1,
      icon: "nlc.png",
      paletteLabel: function() {
        return this.name || "Natural Language Classifier";
      },
      label: function() {
        return this.name || "Natural Language Classifier";
      },
      labelStyle: function() {
        return this.name ? "node_label_italic" : "";
      }
    });
  })();
</script>
```

## 2. ノードの振る舞い表現(nodes/nlc.js)

ノードの振る舞いは同じくnodesディレクトリにjavascriptファイルで配置します。基本的には、ノードオブジェクトを作成し、`on('input', function(msg){})`で前のノードから受け取ったpayloadの処理を記述します。後ろのノードにpayloadを送り出す処理は`send(msg)`です。

```js:nodes/nlc.js
module.exports = function(RED) {

  function NLCNode(config) {
    RED.nodes.createNode(this, config);
    this.username = config.username;
    this.password = config.password;
    this.classifierid = config.classifierid;
    var node = this;

    this.status({fill:"red",shape:"ring",text:"--------"});

    this.on('input', function(msg) {

      if (!msg.payload) {
        node.error('Missing property: msg.payload');
        return;
      }

      var username = this.username;
      var password = this.password;
      if (!username || !password) {
        node.error('Missing service credentials.');
        this.status({fill:"red",shape:"ring",text:"--------"});
        return;
      }

      this.status({fill:"blue",shape:"ring",text:"--------"});

      var watson = require('watson-developer-cloud');
      var nlc = watson.natural_language_classifier({
        url: 'https://gateway.watsonplatform.net/natural-language-classifier/api',
        username: username,
        password: password,
        version: 'v1'
      });

      nlc.classify({
        text: msg.payload,
        classifier_id: this.classifierid },
        function(err, response) {
          if (err) {
            node.error(err);
          } else {
            msg.text = response.text;
            msg.payload = response.top_class;
            msg.classified = response.classes[0].class_name;
            msg.confidence = response.classes[0].confidence;
          }
          node.send(msg);
        });

      this.status({fill:"green",shape:"ring",text:"--------"});

    });
  }

  RED.nodes.registerType("natural-language-classifier", NLCNode, {
    username: { type: "text" },
    password: { type: "password" },
    classifierid: { type: "text" }
  });
}
```

## 3. ノードのアイコン(nodes/icons/nlc.png)

上述したnodes/nlc.htmlの(3)の中に以下のような行があります。

```html:nodes/nlc.html
      icon: "nlc.png",
```

これはエディタ上に表現されるアイコンを示します。このアイコンファイルは、nodes/iconsディレクトリに配置します。

## 4. パッケージへの追加(package.json)

最後に、これまで作成したnodes以下のファイルをパッケージそしてNode-REDに認識させるために、package.jsonに以下のような記述を追加します。

```json:package.json
    "keywords":[ "node-red" ],
    "node-red": {
        "nodes": {
            "natural-language-classifier": "nodes/nlc.js"
        }
    },
```

なお、この例では、NLCを使うためにwatson-developer-cloudパッケージを使っていますので、dependenciesにも以下の行を追加しています。

```json:package.json
    "dependencies": {
        "when": "~3.x",
        "mongodb": "~1.4.x",
        "nano": "~5.11.0",
        "cfenv":"~1.0.0",
        "feedparser":"~0.19.2",
        "redis":"~0.10.1",
        "node-red": "0.x",
        "node-red-bluemix-nodes":"0.x",
        "node-red-node-cf-cloudant":"0.x",
        "node-red-contrib-scx-ibmiotapp":"0.x",
        "node-red-contrib-ibmpush":"0.x",
        "node-red-contrib-bluemix-hdfs":"0.x",
        "node-red-nodes-cf-sqldb-dashdb":"0.x",
        "watson-developer-cloud": "*" # これを追加した(前の行のカンマに注意)
    },
```

# NLC追加版Node-REDを動かす

さて、それでは修正したNode-REDをcf pushします。

```bash
$ cf push weatherqclass
```

いつものように最後にrunningというログが出ればOKです。

```
     state     since                    cpu    memory           disk           details   
#0   running   2015-12-13 08:25:48 PM   0.0%   207.7M of 512M   317.6M of 1G 
```

ブラウザで確認します。

![noderednlc-watson-with-nlc.png](https://qiita-image-store.s3.amazonaws.com/0/21963/c29f85c8-954a-7cd9-009e-6b208d1581b5.png)

無事に「Natural Language Classifier」というノードが追加されました。
早速使ってみます。Injectで文字列を投げ込み、NLCで分類した結果をdebugに吐く、というだけの例です。

![noderednlc-flow.png](https://qiita-image-store.s3.amazonaws.com/0/21963/4de85b00-3726-e259-6712-ec93da8ea609.png)

NLCの設定は以下のようになります。入力されている内容は、サンプル分類器のcredential情報(username, password)と分類器ID (classifier ID)です。

![noderednlc-nlc-config.png](https://qiita-image-store.s3.amazonaws.com/0/21963/f2e12f02-319a-f8c1-a62c-8b0fe55d60fb.png)

このフローを実行する(Injectノードの左側にある四隅の丸い四角のアイコンをクリックする)と、debugログに以下のようにNLCの分類結果が表示されます。

![noderednlc-result.png](https://qiita-image-store.s3.amazonaws.com/0/21963/52fbb297-5455-72ab-980b-33a4e5f60552.png)

このフローの詳細は以下のjsonの通りです。これをインポートすればサンプルフローを再現することができます。

```json
[{"id":"7f2f030e.ba7c14","type":"inject","z":"69f50d6e.27d11c","name":"","topic":"","payload":"Is it cloudy today?","payloadType":"string","repeat":"","crontab":"","once":true,"x":127,"y":154,"wires":[["db86397f.5a46f"]]},{"id":"22108696.9cfe6a","type":"debug","z":"69f50d6e.27d11c","name":"","active":true,"console":"false","complete":"false","x":542,"y":338,"wires":[]},{"id":"db86397f.5a46f","type":"natural-language-classifier","z":"69f50d6e.27d11c","name":"","username":"9b9e911b-538d-4d4a-8f11-5e185e69cca0","password":"qMicYuKpxs6s","classifierid":"3B015Fx14-nlc-2340","x":364,"y":252,"wires":[["22108696.9cfe6a"]]}]
```

# まとめ

* Node-RED StarterにはNatural Language Classifier (NLC)が含まれていませんが、自作のノードを作成することで、Node-REDエディタのフローを簡単に作ることができました。
* 自作のノードを作成するにはある程度javascriptやhtmlの知識をもち、またcfコマンドに慣れている必要があります。
* 実際には学習の工程が存在するため、curlやjqにも慣れておくほうがよさそうです。