snuffkinの遊び場

IT関係、スポーツ、数学等に関することを、気が向いたときに書いてます。

「Jubatus Casual Talks #2 異常検知編」に参加してきました

このところリアルタイム処理が盛り上がってきつつあり、心躍る日々です。そんな中、Jubatus Casual Talks #2 異常検知編に参加してきました。

Jubatus Casual Talks #1が6月だったので、半年ぶりですね。
Jubatus Casual Talks #1に行ってきました & Jubabaは便利! - snuffkinの遊び場

スライドはすぐに公開されると思いますが、私が気になった発表の紹介や簡単な感想等を書きます。

前回のCasual Talkでいただいたご要望に対する進捗状況

NTTの小田哲さん(@oda_satoshi)による発表。
前回のCasual TalkでJubatusに対して多くの要望が挙がったので、その対応の進捗状況の説明。Javaのclient APIの改善、jubadump、エラーメッセージや依存パッケージの見直し等。利用者からの要望に応えて、改善している姿勢は好きです。

0.5.0の新機能(クラスタリング)の紹介

東大M2の村下瑛さん(@akirakiron)による発表。
PFIでJubaclusteringを開発していた方。
Jubatus 0.5.0でクラスタリングが入ったが、アルゴリズムを工夫していて、大量データをクラスタリングできるようにしている。メモリ量はデータサイズnに対してlog(n)、Mixも効果が出るようにしている。

異常検知入門

PFIの比戸将平さん(@sla)による発表。
異常検知の考え方の解説。入門的な話を中心に分かりやすく紹介されていて、私みたいな素人にも面白い内容でした。

Multiple Seasonal Holt-Wintersを実装した話

ニフティ@muddydixonさんによる発表。
サーバリソースの推定等、実際に業務で使っている話が興味深かったです。時系列データをベースライン、トレンド、季節性に分けて変化点検知するとのこと。第x月曜日を考えたりとか、現実データは扱いが難しそうですね。

Jubatus/Storm/Kafkaによるエラー予測システムの検証

キヤノンの松井佑馬さんによる発表。
Kafka+Storm+Jubatusを使い、1万件/秒のエラー予測システムの検証を行ったPoC事例。
ボトルネックはJubatusのRPCを呼び出す部分だそうです。このくらいの件数だと、Storm自体がボトルネックになることはないと思うので、納得。
組み合わせて動かすだけなら簡単だけれど、性能を出すにはプロダクト特性を踏まえた性能チューニングが必要とのこと。チューニングに必要な情報は可視化したいですね。

A use case of online machine learning using Jubatus

NTTデータの下垣徹さん(@shimtoru)による発表。
SUUMOのデモムービーがあって雰囲気が伝わってきました。Jubatusの実案件はこれから増えていくのではないでしょうか。

最後に

Jubatusチームのみなさん、楽しい会をありがとうございました。また、会場を提供してくださった。IIJさんありがとうございました。
第3回も期待しています!

「Vert.xハンズオン with CRaSH」に参加してきました

複数サーバに分散させてスケールさせる必要があるシステムの開発が増えています。このご時世で、ゼロから開発するってことはないと思うので、既存のフレームワークを使いたくなります。Hadoop、Storm、Jubatus等のプロダクトはありますが、これらは特定の目的に特化しており、目的にマッチした場所に使わないと効果が発揮できません。もう少し汎用的に使えそうなのがVert.xだったり、Akkaだったりと考えています。

また、Vert.xはCRaSHのVert.xモジュールと組合せて使うと、さらに便利なことを知りました。CRaSHJVM内にログインしてCLIでいろんな操作ができるので、使いどころが多そうです。

そんなタイミングで、見つけたのがVert.xハンズオン with CRaSHでした。ピッタリなテーマじゃないですか。なので、参加させていただきました。

講師は須江信洋(@nobusue)さん。

次のようなGroovyの本を書かれている方です。

プログラミングGROOVY

プログラミングGROOVY

Groovyイン・アクション

Groovyイン・アクション


ハンズオンの資料は以下で公開されていますので、内容の詳細はそちらをご覧ください。


JGGUGの活動でしたので、サンプルはGroovy版のVert.xサンプルを使って説明されていました。数行書くだけで、pub-sub通信だったりp2p通信だったりができるのは、とても便利。

具体的にサンプルコードを貼ると、こういう風になっています。

  • Receiver.groovy
def eb = vertx.eventBus

eb.registerHandler("news-feed", { message ->
  println "Received news: ${message.body()}"
})
  • Sender.groovy
def eb = vertx.eventBus

// Send a message every second

vertx.setPeriodic(1000) {
  eb.publish("news-feed", "Some news!")
}

いや~これは内部の処理が気になりますね。面白そう。Vert.xがより成長して広まっていく可能性を感じました。

私は普段はJavaが多いので、懇親会でもGroovy/Grails関係の方々とお話できて楽しかったです。
こんなに楽しい技術がたくさんあるのだから、「私も、もっと発信しないと!」と刺激を受けました(^^)
よし、頑張るぞ!

Thinking In Erlang

仕事でミッションクリティカルなシステムを開発しているので、Erlangの存在を知った時は設計思想に興味を持ちました。ですが、仕事でRabbitMQを使っているくらいで、自分でコードを書くこともなくこれまで過ごしていました。
f:id:snuffkin:20131014115602g:plain

そんなところに、Erlangのトレーニングが行われるとの話を聞きつけ、Erlang/OTP トレーニング 2013.10に参加してきました。

講師はフランスから来日中のLoïc Hoguinさん(@lhoguin)。以下のような方です。

  • Erlangの軽量HTTPサーバCowboy作者
  • 仕事でErlangのトレーニングを行っている
  • Erlang User of the Year 2013受賞者!

私はErlang素人なので全然知りませんでしたが、Erlangの世界的な方が来る、ということで楽しみに参加しました。


スライドはここにアップされており、当時利用したコードはここにアップされていますので、内容についてはそちらを見ると良いと思います。
Erlangの考え方を学び、コースコードを書きながら説明してもらえたり、エクササイズがあったりと、濃い一日を過ごすことができました。Loïcさんも優しい感じの雰囲気で、丁寧にディープなことまで教えてくれました。一気にいろいろ学べ、ひとりで読書しているより遥かに効果が大きかったです。

私はJavaをメインに使っているため、Akkaの考え方のベースになっているErlangそのものを学ぶのは非常に興味深かったです。信頼性の高い分散システムを構築するための仕組みが、数多く用意されているのが良いですね。また、atomとパターンマッチングを使ったメッセージ処理も、分かりやすくて気持ち良い。この辺はとても良い所ですね。


特に面白いと思ったのは、observerのstart/0関数Javaで言うとjconsoleみたいな機能。これは便利そうと思いました。次のようなコマンドを打つとGUIで起動します。

observer:start().

起動直後に表示されるSystemタブは、こういう感じ。Erlangのバージョンから、メモリ、CPU、IO等の情報が載っています。
f:id:snuffkin:20131014122551j:plain

次は、Load Chartsタブ。スケジューラ、メモリ、IOの使用率/使用量がグラフで分かります。
f:id:snuffkin:20131014122608j:plain

その次は、Applicationタブ。supervision treeがひとめで分かりますJavaで言うとスレッドの親子関係が分かるイメージ。ノードを右クリックメニューから、各Erlangプロセスの詳細を見たり、メッセージを送信したりと、様々な操作することもできます。これは便利そう。
f:id:snuffkin:20131014122625j:plain

続いて、Processesタブ。各Erlangプロセスのメモリ使用量やキューにあるメッセージ数、実行中の関数が分かります。
f:id:snuffkin:20131014122631j:plain

その後は、Table Viewerタブ。Erlangが持つテーブルの概況が分かるようですが、この画像だとあまりおもしろくなさそうですね。。。
f:id:snuffkin:20131014122638j:plain

最後に、Trace Overview。デバッグに使えるようですが、私は使い方を分かっていません^^;
f:id:snuffkin:20131014122644j:plain

こういう機能は便利だな~と思ったのですが、懇親会で@voluntasさんと話してみると「使わない」とのこと。Erlangデバッグデバッグプリントが王道のようです。そもそも、デバッグプリントで解析できないようなプログラムを書く方が悪いそうです。。。

そうそう、懇親会でこういった方々にErlangの現場の話を聞けたのは良かったです。Erlang界隈の雰囲気も分かりますし、久しぶりの再会があったり。

最後になりますが、会場を提供してくださったVOYAGE GROUPさん、懇親会のピザ・寿司・お酒を提供してくださったBashoさん、ありがとうございました。

面白い内容で、懇親会も活気があり、今後、Erlangのセミナーがあれば、また参加してみたいと思いました。

リアルタイムに最大値・最小値を求めたい~Commons Collections vs Esper

私は新横浜に住んでいるのですが、サッカー好きなので、近所に日産スタジアムがあったり、電車で10分以内に三ツ沢球技場の最寄り駅に着く環境は結構便利です。今日も日産スタジアムではマリノスvsアントラーズ戦が行われているのですが、目の前を通り過ぎて行くサポーターを見ながら、こんなブログを書いています。

add,removeするだけで、最大値や最小値を計算してくれるコレクション~まずは自作

皆さんは「add,removeするだけで、最大値や最小値を計算してくれるコレクション」が欲しいと思ったことはありませんか?
Javaの標準APIでSortedXXXを探して見ると、SortedMapやSortedSetはあるのですが、SortedListが無いんですよね。「コレクションに同じ値を複数格納したい」という条件を付けると、MapやSetは使えないので、これでは困ります。


そこで、自作してみました。それが以下のSimpleTreeMapです。

package jp.gr.java_conf.snuffkin.sandbox.treemap;

import java.util.TreeMap;

public class SimpleTreeMap<T> {
    
    /** key=保持する値, value=同一の値の個数 */
    private TreeMap<T, Integer> treeMap = new TreeMap<T, Integer>();
    
    public void add(T entry) {
        Integer value = treeMap.get(entry);
        
        if (value == null) {
            treeMap.put(entry, 1);
        } else {
            treeMap.put(entry, value + 1);
        }
    }
    
    public void remove(T entry) {
        Integer value = treeMap.get(entry);
        
        if (value != null && value == 1) {
            treeMap.remove(entry);
        } else if (value != null) {
            treeMap.put(entry, value - 1);
        }
    }
}

とっても、素朴に書いてみました。TreeMapを利用して「key=保持する値, value=同一の値の個数」とすることで、複数個の値を持つことができます。まあ、誰でも思いつきそうな実装ですね。

こういうクラスは既に存在するのでは?

でも、こういうクラスは世界中で多くの人が欲しがっているのでは?カジュアルに使うならこれで良いのかもしれないけれど、「秒間数百万イベントを処理する世界で格闘するには、性能を追求したい」とかって欲求を満たすためには、これでは心細いですね。


そこでちょっと探してみると、、、やはり、ありました! 私が見たところ、安心して使えそうなものが2個ありました(他にも良いものがあったらごめんなさい)。

ひとつはApacheから出ているCommons Collections。ここにTreeBagってのがあるじゃないですか。同じ値を複数個保持できるコレクションBagのSortedな実装です。これは使えそうですね。

もうひとつは、OSSのCEPエンジンEsperが最大値・最小値計算に利用しているクラスSortedRefCountedSet。TreeMapをベースにしていて、実装の考え方はSimpleTreeMapとほとんど同じですね。

で、どれが速いの?

とまあ、いつくか選択肢はあるのですが、気になることがありますよね? そう、どれが一番高速なのでしょうか。気になりますね。そこで、以下のプログラムで測定してみました。

package jp.gr.java_conf.snuffkin.sandbox.treemap;

import java.util.Random;

import org.apache.commons.collections.bag.TreeBag;

import com.espertech.esper.collection.SortedRefCountedSet;

/**
 * TreeMapの性能測定クラス。
 * 
 * 事前にDATA_SIZE個のデータをコレクションにaddしておく。
 * ATTEMPT回のadd,removeを繰り返す。
 * add,removeするデータは0からMAX_VALUEまでのintとする。
 */
public class TreeMapBenchmarker {
    
    /** 事前にコレクションにaddしておくデータの個数 */
    private static final int DATA_SIZE = 100;
    /** add,removeするデータの最大値 */
    private static final int MAX_VALUE = 100;
    /** データのadd,removeを繰り返す回数 */
    private static final int ATTEMPT = 100000000;
    
    private Random rand = new Random();
    
    /**
     * 自作のSimpleTreeMapの性能測定
     */
    public void benchmarkSimpleTreeMap() {
        System.out.println("SimpleTreeMap");
        
        // SetUp
        SimpleTreeMap<Integer> collection = new SimpleTreeMap<Integer>();
        for (int index = 0; index < DATA_SIZE; index++) {
            collection.add(rand.nextInt(MAX_VALUE));
        }
        
        // Exercise
        long startTime = System.nanoTime();
        for (int counter = 1; counter <= ATTEMPT; counter++) {
            collection.add(rand.nextInt(MAX_VALUE));
            collection.remove(rand.nextInt(MAX_VALUE));
            
            // Measure
            if (counter % 10000000 == 0) {
                double time = (System.nanoTime() - startTime)/ 1000000000.0;
                System.out.format("counter=%9d, time(sec)=%12.9f\n", counter, time);
            }
        }
        System.out.println();
    }
    
    /**
     * Commons CollectionsのTreeBagの性能測定
     */
    public void benchmarkTreeBag() {
        System.out.println("TreeBag");
        
        // SetUp
        TreeBag collection = new TreeBag();
        for (int index = 0; index < DATA_SIZE; index++) {
            collection.add(rand.nextInt(MAX_VALUE));
        }
        
        // Exercise
        long startTime = System.nanoTime();
        for (int counter = 1; counter <= ATTEMPT; counter++) {
            collection.add(rand.nextInt(MAX_VALUE));
            collection.remove(rand.nextInt(MAX_VALUE), 1);
            
            // Measure
            if (counter % 10000000 == 0) {
                double time = (System.nanoTime() - startTime)/ 1000000000.0;
                System.out.format("counter=%9d, time(sec)=%12.9f\n", counter, time);
            }
        }
        System.out.println();
    }
    
    /**
     * EsperのSortedRefCountedSetの性能測定
     */
    public void benchmarkSrsc() {
        System.out.println("SortedRefCountedSet");
        
        // SetUp
        SortedRefCountedSet<Integer> collection = new SortedRefCountedSet<Integer>();
        for (int index = 0; index < DATA_SIZE; index++) {
            collection.add(rand.nextInt(MAX_VALUE));
        }
        
        // Exercise
        long startTime = System.nanoTime();
        for (int counter = 1; counter <= ATTEMPT; counter++) {
            collection.add(rand.nextInt(MAX_VALUE));
            collection.remove(rand.nextInt(MAX_VALUE));
            
            // Measure
            if (counter % 10000000 == 0) {
                double time = (System.nanoTime() - startTime)/ 1000000000.0;
                System.out.format("counter=%9d, time(sec)=%12.9f\n", counter, time);
            }
        }
        System.out.println();
    }
    
    public static void main(String... args) {
        TreeMapBenchmarker benchmarker = new TreeMapBenchmarker();
        benchmarker.benchmarkSimpleTreeMap();
        benchmarker.benchmarkTreeBag();
        benchmarker.benchmarkSrsc();
    }
}

すると、結果は以下の通りでした。(Core2DuoのMac Book Airで測定したので、最近のマシンならもっと速いです)

SimpleTreeMap
counter= 10000000, time(sec)= 4.274583350
counter= 20000000, time(sec)= 8.392179747
counter= 30000000, time(sec)=12.463048036
counter= 40000000, time(sec)=16.720558747
counter= 50000000, time(sec)=20.822315714
counter= 60000000, time(sec)=24.889912523
counter= 70000000, time(sec)=29.055593668
counter= 80000000, time(sec)=33.289640864
counter= 90000000, time(sec)=37.457229133
counter=100000000, time(sec)=41.541354162

TreeBag
counter= 10000000, time(sec)= 2.637634968
counter= 20000000, time(sec)= 5.240925183
counter= 30000000, time(sec)= 7.845597939
counter= 40000000, time(sec)=10.425084652
counter= 50000000, time(sec)=13.020404733
counter= 60000000, time(sec)=15.613922938
counter= 70000000, time(sec)=18.194127095
counter= 80000000, time(sec)=20.729887176
counter= 90000000, time(sec)=23.281510931
counter=100000000, time(sec)=25.821683679

SortedRefCountedSet
counter= 10000000, time(sec)= 4.430119437
counter= 20000000, time(sec)= 8.851078509
counter= 30000000, time(sec)=13.380416856
counter= 40000000, time(sec)=17.785055448
counter= 50000000, time(sec)=22.205714207
counter= 60000000, time(sec)=26.733082063
counter= 70000000, time(sec)=31.108994779
counter= 80000000, time(sec)=35.716570925
counter= 90000000, time(sec)=40.037819565
counter=100000000, time(sec)=44.373036878

おおっ~、Commons Collectionsがぶっちぎりで最速。私 vs Commons Collections vs Esperは、Commons Collectionsが勝利。
数値的には、Commons CollectionsはSimpleTreeMapより61%速く、SortedRefCountedSetより72%速い、という結果。


Commons Collectionsは「保持している個数をMutableIntegerというクラスで表現していて、個数を更新する際にputしない」とか、高速化の工夫が見られます。こういうのは好きですね。通常の使い方ではあまりやらないことですが、速度を追求する世界だとこういう工夫の積み重ねが大きな差を生みます。


Esperも随所に工夫がみられる良いOSSだと思いますが、(局所的な)このポイントについてはCommons Collectionsに軍配が上がりました。


そうこうしているうちに、日産スタジアムではマリノスが勝ったようです。Twitter等を通じてリアルタイムに情報が伝わるのは、こういう類の技術が裏で支えているからなんですよね。なんとなく、目の前の人波とのつながりを感じました。

Jubatus Casual Talks #1に行ってきました & Jubabaは便利!

Jubatus Casual Talks #1に行ってきました

今日は日産スタジアムでAKB48の総選挙が開催されていました。私の最寄り駅は日産スタジアムのある駅と同じなので、混雑を心配したのですが、混む時間より前に無事帰宅できました。
指原さんがセンターを取ったとのことで思わぬ結果となったそうですが、総選挙の結果って、Jubatusでtwitterを分析したら事前に分かるのでしょうか?そんなことを考えつつ、AKB48の総選挙を横目に、ブログ書いてます^^

ところで、Jubatusのロゴはカワイイですね。ゆるきゃらとか作ったら面白いと思います。

さて、もう開催から一週間経ってしまいますが、6/2(日)にJubatus Casual Talks #1に行ってきました。98名の申し込みに85名の参加だったそうで、こういう会の中では高出席率だったのではないかと思います。

開発チームの方だけでなくJubatusの利用者からの発表もあり、様々な発表を聞くことができ、勉強になりました。私の趣味では、@komiya_atsushiさん「Java で楽して Jubatus したい!」@shimtoruさん「世界征服を目指す Jubatus だからこそ期待する 5 つのポイント」が特に興味深かったです。

Java で楽して〜」はJavaクライアントが使いづらいので、使いやすくしたJubabaというラッパーを作った発表でした。私が携わる案件はJavaを利用することが多く、Javaには馴染み深いです。Jubatus Javaクライアントを触ったことある人なら「ちょっとイケてないよね」と思う所を、「これは使いやすい」にしてくれるのがJubabaです。このエントリの後半で詳しく書きますが、Jubabaいいですね。

「世界征服を目指す〜」は実システムでJubatusを利用するために越えなくてはならない点を、実際に利用した立場から分析しています。SIする側からすると、こういう点はとても重要ですよね。

Jubatus Blogにまとまっていますが、その他の発表も、いろんな立場のいろんな人が発表しており、興味深かったです。また、懇親会で開発チームの方々から興味深い話を聞けたのは良かったです。これだけの規模の運営は大変かと思いますが、是非第2回も開催して欲しいと思います。

Jubabaは便利!

Jubatus Casual Talks #1で興味をもったJubabaを使ってみました。現状はClassifierのみに対応、とのことですので、Jubatusのチュートリアルに載っているClassifier(Java版)をJubabaで書き直してみました。
動かす前にJubatusをインストールする必要がありますが、手っ取り早く使いたい場合は第1回 Jubatus ハンズオンのページからJubatusインストール済みのVirtualBoxVMWareのイメージをダウンロードすると便利です。(Jubatus動かすには困らないのですが、私にはこのVMイメージのrootのパスワードが分かりませんでした。。。)

で、チュートリアルのShogun.javaをJubabaを使って書き直したソースが以下になります。「Javaのコーディング作法的にどうよ」ってツッコミもあるとは思いますが、元のソースを極力尊重しましたので、その辺りはご容赦ください。

package jp.gr.java_conf.snuffkin.sandbox.jubatus.classifier;

import java.util.Collections;
import java.util.List;

import jubaba.Configuration;
import jubaba.Configuration.ConfigurationBuilder;
import jubaba.classifier.BulkClassifier;
import jubaba.classifier.BulkClassifierTrainer;
import jubaba.classifier.ClassificationResult;
import jubaba.classifier.Classifier;
import us.jubat.classifier.Datum;

public class Shogun {
    public static final String HOST = "127.0.0.1";
    public static final int PORT = 9199;
    public static final String NAME = "shogun";

    String[] data1 = {
            "家康", "秀忠", "家光", "家綱", "綱吉",
            "家宣", "家継", "吉宗", "家重", "家治",
            "家斉", "家慶", "家定", "家茂"
    };
    String[] data2 = {
            "尊氏", "義詮", "義満", "義持", "義量",
            "義教", "義勝", "義政", "義尚", "義稙",
            "義澄", "義稙", "義晴", "義輝", "義栄"
    };
    String[] data3 = {
            "時政", "義時", "泰時", "経時", "時頼",
            "長時", "政村", "時宗", "貞時", "師時",
            "宗宣", "煕時", "基時", "高時", "貞顕"
    };

    private Classifier classifier;

    private final void start() throws Exception {
        // 1.Jubatus Serverへの接続設定
        Configuration conf = new ConfigurationBuilder().host(HOST).port(PORT)
            .timeoutSeconds(10).build();
        classifier = new Classifier(conf, NAME);
        
        // 2.学習用データの準備
        BulkClassifierTrainer trainer = makeStudyData();
        
        // 3.データの学習(学習モデルの更新)
        trainer.train();
        
        // 4.予測用データの準備
        BulkClassifier bulkClassifier = makeExData();
        
        // 5.学習データに基づく予測
        List<ClassificationResult> result = bulkClassifier.classify();
        
        // 6.結果の出力
        output(bulkClassifier.data, result);
        return;
    }

    private final BulkClassifierTrainer makeStudyData() {
        String familyName = "";
        
        // ループ処理にて、各将軍の姓と名のセットを作成
        BulkClassifierTrainer trainer = classifier.newBulkClassifierTrainer();
        for (int labelIndex = 0; labelIndex < 3; labelIndex++) {
            String[] nameList = null;
            switch (labelIndex) {
            case 0:
                familyName = "徳川";
                nameList = this.data1;
                break;
            case 1:
                familyName = "足利";
                nameList = this.data2;
                break;
            case 2:
                familyName = "北条";
                nameList = this.data3;
                break;
            }
            
            for (String name : nameList) {
                trainer.newFeatures().label(familyName).add("name", name);
            }
        }
        // 学習用データをシャッフル
        Collections.shuffle(trainer.data);
        
        return trainer;
    }

    private final BulkClassifier makeExData() {
        // ループ処理にて、各将軍の姓と名のセットを作成
        BulkClassifier bulkClassifier = classifier.newBulkClassifier();
        String name = null;
        for (int labelIndex = 0; labelIndex < 3; labelIndex++) {
            switch (labelIndex) {
            case 0:
                name = "慶喜";
                break;
            case 1:
                name = "義昭";
                break;
            case 2:
                name = "守時";
                break;
            }
            bulkClassifier.newFeatures().add("name", name);
        }
        return bulkClassifier;
    }

    private void output(List<Datum> exData, List<ClassificationResult> result) {
        // 結果の出力
        for (int index = 0; index < result.size(); index++) {
            System.out.print(result.get(index).maximumScoredLabel() + " "
                    + exData.get(index).string_values.get(0).second + "\n");
        }
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        new Shogun().start();
        System.exit(0);
    }
}

チュートリアルでは168行あったソースが127行になりました。約25%削減ですね。といっても、相変わらず長いか。
ここで、主張したいポイントは「約25%削減」ではなくて、「Datum周りの実装量の削減」です。チュートリアルの81〜96行目、123〜136行目、147〜160行目をそれぞれ数行に短縮することができました。Jubabaによって、今までJubatusに渡すエンティティの構築に行数を割かれていたのが解消されます。生産性は上がるし、エンジニアのやる気(精神衛生)も上がりますね。

ただ、本家Jubatusが改善されないと、どうにもならないこともあります。実はエンティティクラスにtoStringメソッドがありません。正直、デバッグするのがツライっす。是非、toStringメソッドの実装をお願いします。

という訳でまとめると、JavaでJubatusを使いたい人には、Jubabaはオススメです。使うべきです。Classifier以外の実装も楽しみにしています!