watson-developer-cloudのVisual Recognitionでカスタムの識別器を指定できない件と問題の解決方法

IBMのBluemixが提供する[Watson Visual Recognition](https://www.ibm.com/watson/developercloud/visual-recognition.html)のnode-sdkで遊んでみたところ、カスタムの識別器を使って画像認識ができない！ということに気が付きました。今日は、この問題に気がつくに至った経緯と、その解決方法についてまとめてみます。

# Watson Visual Recognition Serviceとは
Watson Visual Recognition Serviceを使うと、画像を判別することができます。[こちら](http://visual-recognition-demo.mybluemix.net/)にデモがありますが、例えば犬の画像をアップロードすると、「mammal」や「dog」のように、その画像の内容にラベルを振ってくれる、というサービスです。すでに学習済みの識別器が用意されているため、Visual Recognition Serviceの利用者は自分で識別器を作成すること無く、画像を判別することができます。

また、画像とラベルのセットをVisual Recognition Serviceに学習させて、カスタムの画像識別器を作成することもできます。

## Watson Visual Recognition Serviceのカスタム識別器の作成方法
今回、私はサボテンの画像をサボテンと判定するアプリを作ろうと思い、Visual Recognition Serviceで遊んでいました。デフォルトの識別器を試してみたところ、正解率が低かったので、カスタムの識別器も作成することにしました。

学習画像としてGoogle画像検索で「サボテン」を入力し得られる上位50枚の画像を用意し、"cactus.zip"と名前をつけて保存しました。次に、サボテンじゃない画像として、[こちら](http://h.nikkeibp.co.jp/h.jsp?no=269603)から得られる"nega.zip"を拝借しました。BluemixのダッシュボードでVisual Recognition Serviceを作成したあと、用意した画像を使ってカスタムの識別器を作成します。

```bash
$ curl-k -X POST -F "cactus_positive_examples=@cactus.zip" -F "negative_examples=@nega.zip" -F "name=cactus" "https://gateway-a.watsonplatform.net/visual-recognition/api/v3/classifiers?api_key=<Blumixのダッシュボードで得られるapi_key>&version=2016-05-20"
{
    "classifier_id": "cactus_820627908",
    "name": "cactus",
    "owner": "e43f23f9-4118-4d21-8627-a6cdba6ad032",
    "status": "training",
    "created": "2016-10-14T10:02:06.994Z",
    "classes": [{"class": "cactus"}]
}
```

"cactus_820627908"が、作成した識別機のIDになります。作成した識別器を使ってサボテン判定を行うためのパラメータファイルを作成します。下の例では、作成した識別器とVisual Recognition Serviceが用意するデフォルトの識別器を併用しています。

```json:myparams.json
{
    "classifier_ids": ["cactus_820627908", "default"]
}

```

その後、自分で撮影した下のサボテン画像をカスタムの識別器に入力すると。。。
![IMG_4521.JPG](https://qiita-image-store.s3.amazonaws.com/0/16430/1b659f61-d779-d667-c6a0-c81f24856a0f.jpeg)

```bash
$ curl -k -X POST -F "images_file=@IMG_4521.JPG" -F "parameters=@myparams.json" "https://gateway-a.watsonplatform.net/visual-recognition/api/v3/classify?api_key=<Blumixのダッシュボードで得られるapi_key>&version=2016-05-20"
{
    "custom_classes": 1,
    "images": [
        {
            "classifiers": [
                {
                    "classes": [
                        {
                            "class": "blue sky",
                            "score": 0.989013,
                            "type_hierarchy": "/enjoy tropical nature scenes/blue sky"
                        }
                    ],
                    "classifier_id": "default",
                    "name": "default"
                },
                {
                    "classes": [
                        {
                            "class": "cactus",
                            "score": 0.57103
                        }
                    ],
                    "classifier_id": "cactus_820627908",
                    "name": "cactus"
                }
            ],
            "image": "IMG_4521.JPG"
        }
    ],
    "images_processed": 1
}

```

「cactus_820617908」によって「cactus」と判定してもらえました！
ここまでは順調です。


# サボテン画像判定アプリの作成
以上の作業をプログラム的に行うために、Node.jsのサンプルプログラムを作成します。Expressを用いて、アプリの雛形を作成します。私はExpressのバージョン4.13.4を使いました。

```bash
$ express cactus-detection
```

`cd cactus-detection/`したあとに、画像のアップロード先のディレクトリ、パラメータファイル（myparams.json）を保存するディレクトリを作成します。`myparams.json`は`resources`ディレクトリにコピーします。

```bash
$ mkdir uploads
$ mkdir resources
```

`rourtes/index.js`を下のように編集します。

```js:routes/index.js
"use strict";

var express = require('express');
var router = express.Router();
var fs = require('fs');
var watson = require('watson-developer-cloud');
var multer = require('multer');

var storage = multer.diskStorage({
  destination: function(req, file, cb){
    cb(null, './uploads/');
  },
  filename: function(req, file, cb){
    cb(null, Date.now() + '.JPG');
  }
});

var upload = multer({storage: storage}).single('thumbnail');

var visual_recognition = watson.visual_recognition({
  api_key: '<Bluemixのダッシュボードで得られるapi_key>',
  version: 'v3',
  version_date: '2016-05-20'
});

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

/* Classify the uploaded image */
router.post('/api/classify', function(req, res) {
  upload(req, res, function(err) {
    if(err){
      res.send("Failed to write " + req.file.destination + " with " + err);
    }else{
      console.log("uploaded " + req.file.originalname + " as " + req.file.filename + ". Size: " + req.file.size);
      console.log("uploaded file is in " + req.file.path);

      // Call Watson API
      var params = {
        images_file: fs.createReadStream('./uploads/' + req.file.filename),
        parameters: fs.createReadStream('./resources/myparams.json')
      };

      visual_recognition.classify(params, function(err, response) {
        if(err){
          console.log(err);
          res.send(err);
        }else{
          res.send(JSON.stringify(response, null, 2));
        }
      });
    }
  });
});

module.exports = router;
```

また、アプリのインターフェースも用意します。

```jade:views/index.jade
extends layout

block content
  h1= title
  form(method="post", enctype="multipart/form-data", action="/api/classify")
    input(type="file", name="thumbnail")
    input(type="submit")
```

アプリの準備ができました！では、先程`curl`コマンドに入力したのと同じ画像を使って、サボテン画像を判別してみましょう。まずはアプリを起動します。

```bash
$ npm start

> cactus-detection@0.0.0 start C:\Users\xxxxx
> node ./bin/www

```

ブラウザ上で[http://localhost:3000](http://localhost:3000)にアクセスし、`curl`コマンドに渡したのと同じ画像を選んで送信ボタンを押すと。。。

![gui.png](https://qiita-image-store.s3.amazonaws.com/0/16430/86fda501-4a8a-1745-75d0-f765bf736e4f.png)

下のようなjsonがブラウザに表示されました。あれれ、コマンドラインの実行結果と違いますね。デフォルトの識別器の判定結果しか返ってきません。

```json
{ "custom_classes": 0, "images": [ { "classifiers": [ { "classes": [ { "class": "blue sky", "score": 0.989013, "type_hierarchy": "/enjoy tropical nature scenes/blue sky" } ], "classifier_id": "default", "name": "default" } ], "image": "1476441268403.JPG" } ], "images_processed": 1 }
```

# 何が悪かったのか
いくら試行錯誤しても、カスタムの識別器の判定結果が返ってきません。なぜなのか。。。
Visual Recognitionのnode-sdkのコードを読んでみました。

```js:node-sdk/visual-recognition/v3.js
VisualRecognitionV3.prototype.classify = function(params, callback) {

...

  params = extend({
    classifier_ids: ['default'],
    owners: ['me','IBM']
  }, params);

  var parameters;

  if(params.images_file) {
    parameters = {
      options: {
        url: '/v3/classify',
        method: 'POST',
        formData: {
          images_file: params.images_file,
          parameters: {
            value: JSON.stringify(pick(params, ['classifier_ids', 'owners', 'threshold'])),
            options: {
              contentType: 'application/json'
            }
          }
        },
...
```

パラメータファイルの`myparams.json`は、上のコードのparams.parameterに`fs.createReadStream`オブジェクトとして格納されています。一方で、formDataのparametersでは、JSON文字列を渡そうとしています。しかし、上のコードでは`myparams.json`の中身は読んでいません。`myparams.json`でいくらカスタムの識別器を指定しても、デフォルトのパラメータしか渡っていなかったのです！

そこで、Visual Recognition Serviceのnode-sdkを下のように修正してみました。`cactus-detection/node_modules/watson-developer-cloud/visual-recognition/v3.js`に保存されているファイルを編集します。`params.parameters`が指定されれば、指定されたパラメータを使う、指定されなければデフォルトのパラメータを使う、というロジックです。

```js:node-sdk/visual-recognition/v3.js
VisualRecognitionV3.prototype.classify = function(params, callback) {

...

  params = extend({
    classifier_ids: ['default'],
    owners: ['me','IBM']
  }, params);

  var parameters;
  var param_value;

  if(params.parameters) {
    param_value = params.parameters;
  }else{
    param_value = JSON.stringify(pick(params, ['classifier_ids', 'owners', 'threshold']));
  }

  if(params.images_file) {
    parameters = {
      options: {
        url: '/v3/classify',
        method: 'POST',
        formData: {
          images_file: params.images_file,
          parameters: {
            value: param_value,
            options: {
              contentType: 'application/json'
            }
          }
        },
...
```

このコードを使ってアプリを動かしてみると。。。

```json
{ "custom_classes": 1, "images": [ { "classifiers": [ { "classes": [ { "class": "blue sky", "score": 0.989013, "type_hierarchy": "/enjoy tropical nature scenes/blue sky" } ], "classifier_id": "default", "name": "default" }, { "classes": [ { "class": "cactus", "score": 0.57103 } ], "classifier_id": "cactus_820627908", "name": "cactus" } ], "image": "1476603943224.JPG" } ], "images_processed": 1 }
```

めでたく、`cactus_820627908`による識別結果が返ってきました！

# まとめ
Watson Visual Recognitionのnode-sdkを修正して、カスタムの識別器を指定して画像判別ができるようになりました。今日（2016/10/16）時点で、node-sdkに新たな修正は入っていないので、依然としてカスタムの識別器を指定できない、という問題は残っていると思われます。早く直ると良いですね。