マルチラベル分類用のライブラリMulanをjythonから使ってみる

マルチラベル問題とは,「ひとつの事例に複数のラベルがつきうる場合の問題」のことを指す.
例えば,文書分類だと,「サッカーのフーリガンのニュース記事はスポーツのジャンルだし,社会のジャンルでもある」っていうラベルが付く場合を指す.

こういうマルチラベル問題の解き方はいろいろあって,マルチラベルを考慮したアルゴリズムもあれば,問題を分割して二値分類で解いてしまうケースもあって,実に様々.

こんな様々な解き方があるのでは,ひとつひとつの手法を試すのにいちいち実装が大変...というモチベーションで生まれた(たぶん)のがMulanというJavaのライブラリだ.
Mulan: A Java library for multi-label learning
※このJavaライブラリは機械学習ライブラリのwekaの上に作られているので,wekaを使える状態にしておかないといけない.

ここで問題がひとつ.ぼくはJavaが書けない.
そこで,ぼくがとるべき方法は2つあった.
1 Javaの書き方を勉強する
2 めんどいのでJythonで解決する

めんどいので2にした.

MulanのdocumentGetting Started with Mulanにしたがって,MulanExp1.javapythonスクリプトに書き直ししてみようとしたが,2つの点で軽くはまった.

1 Jython実行時のクラスパスの通し方

javaコードは,他のjarファイルを参照している時には,クラスパスを通すという作業を実行時にしないといけない.
ネット上に転がっている説明だと

java -cp hoge;hoge;. 実行するjavaクラス

みたいな説明がされているが,これをそのまま鵜呑みにするとハマる.

まず,セミコロン区切りはwindows特有のものらしい.Linuxで実行するときは,コロンで区切らないといけない.なので,Linuxでは

java -cp hoge:hoge:. 実行するjavaのクラス

となる.

さらにjythonで実行時には-cpの前に-Jを付けないといけない.この-Jとは「これ以降はjavaコンパイラに渡す引数だからね」を意味しているらしい.
だから,jythonコンパイラを呼び出すときには

jython -J-cp hoge:hoge huga.py

になる.

以上を踏まえて,mulanとwekaにクラスパスを通しつつjythonコンパイラを起動するには

kensuke-mi@pine12:/work/kensuke-mi/tmp_working/tmp_mulan/mulan-1.4.0$ jython -J-cp mulan.jar:weka-3.7.6.jar
Jython 2.7b1 (default:ac42d59644e9, Feb 9 2013, 15:24:52)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)] on java1.7.0_45
Type "help", "copyright", "credits" or "license" for more information.
>>> import Mulan
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named Mulan
>>> import mukan
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named mukan
>>> import mulan
>>> import weka

とすればいい.

2 jythonスクリプトでimportの記述方法

javaのコードでは,ライブラリのimportは

import クラス名

でいいらしいが,jython使用時には違う.

from クラスパス import クラス名

と記述しないといけない.
具体例を出すと,

Javaコードでは,これが

import weka.core.Utils;

Jyhonコードだと,こうなる

from weka.core import Utils;

以上,1と2のハマりポイントを踏まえながら,MulanExp1.javaをMulanExp1.pyに書き直すと以下のようになる.

# -*- coding:utf-8
# java library from here
from weka.classifiers.trees import J48;
from weka.core import Utils;
from mulan.classifier.lazy import MLkNN;
from mulan.classifier.meta import RAkEL;
from mulan.classifier.transformation import LabelPowerset;
from mulan.data import MultiLabelInstances;
from mulan.evaluation import Evaluator;
from mulan.evaluation import MultipleEvaluation;
#Python library from here
import sys;

#arffFilename = weka.core.Utils.getOption("arff", args); # e.g. -arff emotions.arff
#xmlFilename = weka.core.Utils.Utils.getOption("xml", args); # e.g. -xml emotions.xml
arffFilename='./emotions.arff';
xmlFilename='./emotions.xml';

dataset = MultiLabelInstances(arffFilename, xmlFilename);

learner1 = RAkEL(LabelPowerset(J48()));
learner2 = MLkNN();

eval = Evaluator();belPowerset(J48()));

numFolds = 10;
results = eval.crossValidate(learner1, dataset, numFolds);
#System.out.println(results);
print results;
results = eval.crossValidate(learner2, dataset, numFolds);
#System.out.println(results);
print results;

ただ,気になるのが一点.
printした時に,一部の文字が?に置き換わってしまっている.
たぶん,javaコンパイラpythonの文字仕様が違うから?と思うけども...いまのところよくわからない.
>>追記

おれはJython第1回に解決策が書いてあった.ズバリ,javaの文字出力命令を使ってしまえばよい.
具体的にはheaderに

from java.io import *;
from java.lang import *;

を記述して,print文の代わりに,

System.out.println(result)

と記述すればいい.

ついでに,MulanExp2.pyはこうなる

#-*- coding:utf-8 -*-
from java.io import FileReader;
from mulan.classifier import MultiLabelOutput;
from mulan.classifier.meta import RAkEL;
from mulan.classifier.transformation import LabelPowerset;
from mulan.data import MultiLabelInstances;
from weka.classifiers.trees import J48;
from weka.core import Instance;
from weka.core import Instances;
from weka.core import Utils;

arffFilename='./emotions.arff';
xmlFilename='./emotions.xml';
dataset = MultiLabelInstances(arffFilename, xmlFilename);

RAkEL_model = RAkEL(LabelPowerset(J48()));

RAkEL_model.build(dataset);

#unlabeledFilename = Utils.getOption("unlabeled", xmlFilename);
unlabeledFilename='./emotions.arff';
reader = FileReader(unlabeledFilename);
unlabeledData = Instances(reader);

numInstances = unlabeledData.numInstances();
#for (int instanceIndex = 0; instanceIndex < numInstances; instanceIndex++) {
for instanceIndex in range(numInstances):

    instance = unlabeledData.instance(instanceIndex);
    output = RAkEL_model.makePrediction(instance);
    # do necessary operations with provided prediction output, here just print it out
    #System.out.println(output);
    print output;