KNIMEエクステンション開発

dodosoft Advent Calendar、uphy担当3記事目! www.adventar.org

ここ最近KNIME触ってます。 KNIMEは機械学習やデータの処理をGUIのぽちぽちで実現できるものです。

KNIME | KNIME Analytics Platform

KNIMEは、日立製作所のLumadaの構成要素の一つにもなっているようで、一部の人たちの間で熱いです。 http://www.hitachihyoron.com/jp/pdf/2016/07_08/2016_07_08_10.pdf

KNIMEで社内のAPIを入力としたかったのですが、それを実現するようなノードがなかったので、エクステンション(Eclipseプラグイン)を開発しました。今回はその手順を記事にします。 記事ではその実物ではなく、簡略化したものを掲載します。

KNIMEはEclipse RCPアプリケーションであるため、プラグインを開発することで、誰でも簡単に拡張できます。

開発対象

ダミーWeb APIからデータを取得する、KNIMEノードを開発します。

ダミーWeb API

ここは今回のテーマから外れるので、重要ではないです。

固定のJSONを返すダミーのWeb APIを実行しておきます。
このWeb APIから、データを取得するKNIMEノードを開発します。

dummyapi.py

from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/api", methods=['GET'])
def api():
    columns = ['A', 'B', 'C']
    rows = []
    for i in range(1000):
        row = {}
        for col in columns:
            row[col] = "%d/%s" % (i, col)
        rows.append(row)
    return jsonify({
        'columns': columns,
        'rows': rows
    })

app.run(host='0.0.0.0')

実行。

$ python dummyapi.py

開発環境構築

今回は、KNIME SDKを利用します。

KNIME | Download KNIME Analytics Platform & SDK

別の手段として、EclipseプラグインとしてKNIMEの開発キットをインストールすることもできます。 上記のKNIME SDKは、開発キットのプラグインをインストール済みのEclipseです。

プロジェクト作成

  1. File > New
  2. KNIME > Create a new KNIME Node-Extension f:id:uphy:20161216214054p:plain
  3. Finish

サンプルの実行

上記の設定で、サンプル込みのプロジェクトが生成されるので、サンプルをまず実行してみます。

  1. 作成したプロジェクトを右クリック
  2. Run > Eclipse Application
  3. 開いたもう一つのEclipseは、サンプルのプラグインを含むEclipseです。Window > Perspectives > Open Perspectiveから、KNIMEパースペクティブを開きます。
  4. KNIME ExplorerのLOCALを右クリックして、New KNIME Workflow Wizardをクリック
  5. Finish
  6. 開いたキャンバスに、Node RespositoryのWebApiCallノードをドラッグアンドドロップ

サンプルのビルド

プラグインのjarファイルを生成して、KNIMEにインストールしてみます。

  1. プロジェクトを右クリック
  2. Export
  3. Plug-in Development > Deployable plug-ins and fragmentsを選択してNext
  4. 出力先のDirectoryを選択してFinish 出力先に、jarファイルが生成されます。
  5. KNIMEのdropinsにjarファイルを入れる。Macの場合はアプリケーションがパッケージ化されているので、KNIME X.Y.Z.appを右クリックして、「パッケージの内容を表示」をして、Contents/Eclipse/dropinsに入れてください。
  6. KNIMEを起動

WebApiCallノードが追加されたことを確認しました。 これでサンプルの実行&プラグインのビルドができたので、これをベースにプラグインを開発します。

開発

ノード定義

まずは見た目から。
WebApiCallNodeFactory.xmlで各種説明を入力します。 名前が気になったので、Web API Callと変更しました。 また、作成するノードはデータの入力は行わないので、inPortを削除しました。

ここで一旦実行します。

f:id:uphy:20161216214933p:plain

Node Descriptionビューにノードの説明が表示されました!

モデル定義

WebApiCallNodeModelを修正。

ノードの入力数、出力数を、コンストラクタで指定します。

    /**
     * Constructor for the node model.
     */
    protected WebApiCallNodeModel() {
        super(0, 1);
    }

モデルのパラメータを定義します。
KNIMEのノードをダブルクリックしたときの設定項目を全てパラメータとして定義します。
ここでは、endpointUrlを定義します。

    static final String ENDPOINT_URL_KEY = "endpointUrl";
    static final String ENDPOINT_URL_DEFAULT = "http://localhost:5000/api";

    private final SettingsModelString m_endpointUrl = new SettingsModelString(ENDPOINT_URL_KEY, ENDPOINT_URL_DEFAULT);

ダイアログの画面定義でもKEY、DEFAULTは参照するので、定数として定義しています。

ダミーAPIからのデータ(JSON)をパースするために外部ライブラリ(org.json:json)を利用します。
取得はこのあたりからでも。

Maven Repository: org.json » json » 20160810

取得したライブラリをClasspathに追加します。plugin.xmlを開いてRuntimeタブでAdd...をクリックして下記のように設定。下記ではプロジェクト直下にlibディレクトリを作成し、そのなかに上記のライブラリを配置しました。

f:id:uphy:20161230084011p:plain

引き続きWebApiCallNodeModelに戻って、ダミーAPIからデータを取得するコード。

   private JSONObject callApi(final URL url) throws IOException {
        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (InputStream inputStream = url.openStream()) {
        final  byte[] buf = new byte[0x1000];
            int size;
            while ((size = inputStream.read(buf)) != -1) {
                baos.write(buf, 0, size);
            }
        }
        return new JSONObject(new String(baos.toByteArray()));
    }

そのデータを、KNIMEのデータに変換するコードを書きます。

   @Override
    protected BufferedDataTable[] execute(final BufferedDataTable[] inData, final ExecutionContext exec)
            throws Exception {
        final URL url = new URL(this.m_endpointUrl.getStringValue());
        final JSONObject response = callApi(url);

        final JSONArray jsonColumns = response.getJSONArray("columns");
        final DataColumnSpec[] allColSpecs = new DataColumnSpec[jsonColumns.length()];
        for (int i = 0; i < allColSpecs.length; i++) {
            allColSpecs[i] = new DataColumnSpecCreator(jsonColumns.getString(i), StringCell.TYPE).createSpec();
        }

        final DataTableSpec outputSpec = new DataTableSpec(allColSpecs);
        final BufferedDataContainer container = exec.createDataContainer(outputSpec);
        final JSONArray jsonRows = response.getJSONArray("rows");
        for (int i = 0; i < jsonRows.length(); i++) {
            final JSONObject jsonRow = jsonRows.getJSONObject(i);
            final RowKey key = new RowKey("Row " + i);
            final DataCell[] cells = new DataCell[jsonColumns.length()];
            for (int j = 0; j < cells.length; j++) {
                cells[j] = new StringCell(jsonRow.getString(jsonColumns.getString(j)));
            }
            final DataRow row = new DefaultRow(key, cells);
            container.addRowToTable(row);
            exec.checkCanceled();
            exec.setProgress(i / (double) jsonRows.length(), "Adding row " + i);
        }
        container.close();
        BufferedDataTable out = container.getTable();
        return new BufferedDataTable[] { out };
    }

ビュー定義

WebApiCallNodeDialog.javaを変更します。

   protected WebApiCallNodeDialog() {
        super();
        addDialogComponent(new DialogComponentString(
                new SettingsModelString(WebApiCallNodeModel.ENDPOINT_URL_KEY, WebApiCallNodeModel.ENDPOINT_URL_DEFAULT),
                "Endpoint URL", true, 30));
    }

簡単!! こんなダイアログができました。 f:id:uphy:20161217001046p:plain

実行

サンプルと同様の手順で実行できます。

ダミーAPIからデータを取得できました。

f:id:uphy:20161217001122p:plain

ビルド

サンプルと同様の手順でビルドできます。

Mavenプロジェクト化

uphy.hatenablog.com

まとめ

ダミーのWeb APIからデータを取得するKNIMEノードを作成しました。
コードは良くも悪くも素直で書きやすかったです。
もうちょっとアノテーション活用するとか、DIいれるとかしてくれるとより開発しやすくて助かります。