Oracleから公開されたTribuoをやってみた。Tribuo - A Java prediction library (v4.0)

実際にやってみた。
結論から言うととっても不安になった。あと、フレームワークを名乗るなら、英語でいいからjavadocはちゃんと書け。
それから、マニュアル直すのではなく、実装コードをきちんと直したほうがいいぞ。

#セットアップ
依存性の解決にはmavenを使用しました。プロジェクト作成後、下記のようにtribuoをpomファイルに設定します。

```
<dependency>
    <groupId>org.tribuo</groupId>
    <artifactId>tribuo-all</artifactId>
    <version>4.0.0</version>
    <type>pom</type>
</dependency>
```

ロジスティック回帰モデルを学習し、評価するには[下記のようにすればよい](https://qiita.com/jashika/items/bc03dbbca92ba38f4502)と書かれているため、下記のコードをjavaクラスに記述します。

```
var trainSet = new MutableDataset<>(new LibSVMDataSource("train-data",new LabelFactory()));
var model    = new LogisticRegressionTrainer().train(trainSet);
var eval     = new LabelEvaluator().evaluate(new LibSVMDataSource("test-data",trainSet.getOutputFactory()));
```

クラス名はTribuoSampleとしました。
は？ナニコレ？

![コンストラクタがないエラー01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/732297/8c629560-587f-1890-9310-a5c60397b11c.png)

LibSVMDataSourceのコンストラクタ部分です。Stringを引数にとるコンストラクタはないです。

![LibSVMDataSourceコンストラクタ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/732297/b13f180f-c164-6c0c-01ff-1f74bb1296eb.png)

しかもこのjava.nio.file.Pathインターフェースって?
java.nio.file.Pathsのほうで実装ではないということは・・・。
もしかして、Path.ofとスタティックにアクセスしているからモダンなのか?


```java:
        // こう書けって言ってるの？
        var trainSet = new MutableDataset<>(new LibSVMDataSource<>(Path.of(new URL("file:train-data").getPath()),new LabelFactory()));

        // それともこう？
        var trainSet = new MutableDataset<>(new LibSVMDataSource<>(Paths.get("train-data"),new LabelFactory()));

        // だったらURLでいいんでない？冗長だし。
        var trainSet = new MutableDataset<>(new LibSVMDataSource<>(new URL("file:train-data"),new LabelFactory()));
```

マニュアル直すか、このクラス本体でStringを受け付けるかになるが、MutableDataset.classを直すべきでしょうね。こんな感じでとりあえずできるでしょうし。

```java:MutableDataset

    public LibSVMDataSource(String url, OutputFactory<T> outputFactory) throws IOException {
        this(null,new URL(url),outputFactory,false,false,0);
    }

    public LibSVMDataSource(String url, OutputFactory<T> outputFactory, boolean zeroIndexed, int maxFeatureID) throws IOException {
        this(null,new URL(url),outputFactory,true,zeroIndexed,maxFeatureID);
    }

```

ただ、コンストラクタの処理で、urlも、pathもチェックしてないんですよね。
下記がMutableDataset.classの該当コード。

```java:LibSVMDataSource
    private LibSVMDataSource(Path path, URL url, OutputFactory<T> outputFactory, boolean rangeSet, boolean zeroIndexed, int maxFeatureID) throws IOException {
        this.outputFactory = outputFactory;
        this.path = path;
        this.url = url;
        this.rangeSet = rangeSet;
        if (rangeSet) {
            this.zeroIndexed = zeroIndexed;
            this.minFeatureID = zeroIndexed ? 0 : 1;
            if (maxFeatureID < minFeatureID + 1) {
                throw new IllegalArgumentException("maxFeatureID must be positive, found " + maxFeatureID);
            }
            this.maxFeatureID = maxFeatureID;
        }
        read();
    }

```
どこまで行っちゃうのかっていうと、
コンストラクタから呼ばれるLibSVMDataSource#readの中。
下記が該当のコード。

```java:LibSVMDataSource#read

    private void read() throws IOException {
        int pos = 0;
        ArrayList<HashMap<Integer,Double>> processedData = new ArrayList<>();
        ArrayList<String> labels = new ArrayList<>();

        // Idiom copied from Files.readAllLines,
        // but this doesn't require keeping the whole file in RAM.
        String line;
        // Parse the libsvm file, ignoring malformed lines.
        try (BufferedReader r = new BufferedReader(new InputStreamReader(url.openStream(),StandardCharsets.UTF_8))) {
            for (;;) {
                line = r.readLine();
                if (line == null) {
                    break;
                }
                pos++;
                String[] fields = splitPattern.split(line);
                try {
                    boolean valid = true;
                    HashMap<Integer, Double> features = new HashMap<>();
                    for (int i = 1; i < fields.length && valid; i++) {
                        int ind = fields[i].indexOf(':');
                        if (ind < 0) {
                            logger.warning(String.format("Weird line at %d", pos));
                            valid = false;
                        }
                        String ids = fields[i].substring(0, ind);
                        int id = Integer.parseInt(ids);
                        if ((!rangeSet) && (maxFeatureID < id)) {
                            maxFeatureID = id;
                        }
                        if ((!rangeSet) && (minFeatureID > id)) {
                            minFeatureID = id;
                        }
                        double val = Double.parseDouble(fields[i].substring(ind + 1));
                        Double value = features.put(id, val);
                        if (value != null) {
                            logger.warning(String.format("Repeated features at line %d", pos));
                            valid = false;
                        }
                    }
                    if (valid) {
                        // Store the label
                        labels.add(fields[0]);
                        // Store the features
                        processedData.add(features);
                    } else {
                        throw new IOException("Invalid LibSVM format file");
                    }
                } catch (NumberFormatException ex) {
                    logger.warning(String.format("Weird line at %d", pos));
                    throw new IOException("Invalid LibSVM format file", ex);
                }
            }
        }
```

tryの中でurl.openStream()ってやっちゃってるし。キャッチするのNumberFormatExceptionだけだしなぁ。
メンバ変数のdescriptionを見ると、urlかpathのどちらかが必須になっているけど。

```java:LibSVMDataSource
    // url is the store of record.
    @Config(description="URL to load the data from. Either this or path must be set.")
    private URL url;

    @Config(description="Path to load the data from. Either this or url must be set.")
    private Path path;
```

LibSVMDataSource#postConfigで両方nullだったらのチェックはしているけど、これじゃダメじゃん。

```java:LibSVMDataSource#postConfig
    @Override
    public void postConfig() throws IOException {
        if (maxFeatureID != Integer.MIN_VALUE) {
            rangeSet = true;
            minFeatureID = zeroIndexed ? 0 : 1;
            if (maxFeatureID < minFeatureID + 1) {
                throw new IllegalArgumentException("maxFeatureID must be positive, found " + maxFeatureID);
            }
        }
        if ((url == null) && (path == null)) {
            throw new PropertyException("","path","At most one of url and path must be set.");
        } else if ((url != null) && (path != null) && !path.toUri().toURL().equals(url)) {
            throw new PropertyException("","path","At most one of url and path must be set");
        } else if (path != null) {
            // url is the store of record.
            try {
                url = path.toUri().toURL();
            } catch (MalformedURLException e) {
                throw new PropertyException(e,"","path","Path was not a valid URL");
            }
        }
        read();
    }
```

こんなコード書いたら誰も処理しないよね。

```java:TribuoSample
public class TribuoSample {

    /**
     * @param args mainメソッドの引数。
     */
    public static void main(String[] args) {

        URL url = null;

        try {
            var trainSet = new MutableDataset<>(
                    new LibSVMDataSource<>(url, new LabelFactory()));
        } catch (IOException e) {
            // TODO 自動生成された catch ブロック
            e.printStackTrace();
        }
    }
}
```

これを実行すると・・・。

```:StackTrace
Exception in thread "main" java.lang.NullPointerException
	at org.tribuo.datasource.LibSVMDataSource.read(LibSVMDataSource.java:204)
	at org.tribuo.datasource.LibSVMDataSource.<init>(LibSVMDataSource.java:125)
	at org.tribuo.datasource.LibSVMDataSource.<init>(LibSVMDataSource.java:105)
	at org.project.eden.adam.TribuoSample.main(TribuoSample.java:28)
```

この場合、利用者に伝えたいメッセージは、「urlかpathのいずれかが設定されていることは必須の項目なんだけど、
あなたがurlに設定した値にはnullが設定されていたよ。」ってことを伝えなければならないはずなんだけど、想定していないエラーだかスタックトレース吐き出して処理が止まってしまうわけだ。業務アプリならまだしも、oracleの名前でだしたフレームワークなのだから、このような落ち方はいかがなものかと思う。pathの場合には、コンストラクタでpathオブジェクトにアクセスしちゃうから、その場でヌルポだしね。

これがその実装

```java:LibSVMDataSource
    public LibSVMDataSource(Path path, OutputFactory<T> outputFactory) throws IOException {
        this(path,path.toUri().toURL(),outputFactory,false,false,0);
    }
```

サンプルを下記のようにして実行。

```java:TribuoSample 
public class TribuoSample {

    /**
     * @param args mainメソッドの引数。
     */
    public static void main(String[] args) {

        Path path = null;

        try {
            var trainSet = new MutableDataset<>(
                    new LibSVMDataSource<>(path, new LabelFactory()));

        } catch (IOException e) {
            // TODO 自動生成された catch ブロック
            //e.printStackTrace();
        }
    }
}
```

結果は見るまでもなく、NullPointerException。

```
Exception in thread "main" java.lang.NullPointerException
	at org.tribuo.datasource.LibSVMDataSource.<init>(LibSVMDataSource.java:97)
	at org.project.eden.adam.TribuoSample.main(TribuoSample.java:28)

```

ドキュメントのトップページに出てくる、1行目からこんなになってるとは思わなかった。
サンプル実行は別のページで。