【Java初心者】Wekaによる機械学習 07-自然言語処理を使ったテキストの現実的な分類

今回も、試行錯誤の結果を自分用にメモしたものです。
間違っているところがありましたら、ご指摘ください。

# 目的

やりたいことは
* 短いテキストデータを教師データとし、行ごとのカテゴリーをつけておく
* 上記データでモデルを作って保存
* 分類が必要なデータ(本番データ)は別にあり、上記モデルを利用する

# 用意するアプリ

* 自分はJavaアプリから利用するので、今回もmini-wekaを使います。https://mvnrepository.com/artifact/com.github.fracpete/mini-weka

# 用意するデータ

* 教師データ。いろいろなニュース記事の中から、ロサンゼルス近郊の火災の記事と、国内の記事を一文ずつあつめて形態素解析で分かち書きにしておく。(自分は IgoというJavaのライブラリーと、ipadoc + Neologdを使いました)カンマで区切って「国際」または「国内」というカテゴリーをつける。train.csvとして保存。
* ストップワード。(ただし、上記教師データに即したものです。本当はきちんと精査するべきですがか、説明用として作りました)stop.txtとして保存。
* 本番データ(テストデータ)。教師データと似たような形式のCSV。(説明用にテストが成功しやすいものにしています)

カレントディレクトリに data というディレクトリを作り、その下に csv , arff , model , dic という4つのサブディレクトリをつくりました。csvデータは csvに。ストップワードは dic に入れます。もちろんコードの書き方に応じて変更可能です。

教師データ(data/csv/train.csv)の例
```csvs
原稿,カテゴリー
ドジャース が 本拠地 を 置く ロサンゼルス が 非常事態 に 見舞わ れ て いる 。 ,国際
特に ロサンゼルス 西部 の 「 パリ セーズ 火災 」 や 北部 近郊 の アルタデナ 周辺 で 発生 し た 「 イートン 火災 」 で は 、 約 110 平方キロ が 焼け 、 鎮圧 の メド は 立っ て い ない 。 ,国際
すでに 死者 も 5人 出 て おり 、 バイデン 大統領 は この 日 「 過去最悪 の 山火事 だ 」 と 述べ た 。 ,国際
米 カリフォルニア州 ロサンゼルス 西部 で 大規模 な 山火事 が 続い て いる 。 ,国際
9日 まで に 5人 の 死亡 が 確認 さ れ た 。 当初 は 7人 と 発表 さ れ て い た が 、 ロサンゼルス郡 の ロバート ・ ルナ 保安官 は 、 イートン 地区 で の 死者 が 下方 修正 さ れ た ため 、 実際 に は 5人 だ と 説明 し た 。 ,国際
英国 の ヘンリー王子 ( 40 ) と メーガン妃 ( 43 ) 夫妻 が 9日 、 米 カリフォルニア州 ロサンゼルス を 襲っ た 壊滅 的 な 山火事 で 被災 し た 人々 へ の 支援 を 訴え た 。 ,国際
「 南カリフォルニア の 山火事 」 と 題する 声明 を 夫妻 の 公式ホームページ に 掲載 。 ,国際
「 ここ 数 日 、 南カリフォルニア の 山火事 が 近隣 地域 を 襲い 、 家族 、 学校 、 医療センター 、 その他 多く の もの を 破壊 し 、 あらゆる 階層 の 数 万 人 に 影響 を 与え て い ます 」 と つづり 、 温かい 食事 を 提供 する 慈善団体 や 被災 し た 動物 を 世話 する 団体 など 現地 で 支援 活動 を する 団体 の リスト を 共有 し た 。 ,国際
米 西部 カリフォルニア州 ロサンゼルス 近郊 の 複数 地域 で 発生 し た 山火事 で 、 地元当局 は 9日 、 損壊 し た 家屋 など の 建物 が 計 9000 棟 以上 と み られる と 発表 し た 。 ,国際
７ 日 に 発生 し た 火災 は 、 強風 や 干ばつ により 消火活動 が 難航 。 9日 に も 新た な 山火事 で 避難命令 が 出 て おり 、 予断 を 許さ ない 状況 が 続く 。 ,国際
バイデン 大統領 は 9日 、 ホワイトハウス で 会議 を 招集 し 、 「 カリフォルニア 史上 、 最も 広範囲 にわたって 甚大 な 被害 を もたらし て いる 火災 だ 」 と 指摘 。 ,国際
埼玉県川口市 は 、 市内 の ご み処理 施設 で 起き た 火災 の 影響 で 、 9日 と 10日 の 2日間 、 一般 ごみ の 収集 を 停止 し まし た 。 ,国内
市 は 近隣 の 自治体 など に 協力 を 依頼 し て 、 週明け の 今月13日 から 収集 を 再開 する と 発表 し まし た 。 ,国内
今月3日 、 市内 の ご み処理 施設 「 朝日環境センター 」 で 火災 が あり 、 電気 系統 や ごみ を 運ぶ クレーン など が 被害 を 受け まし た 。 ,国内
川口市 は 市内 に ある もう 1つ の ご み処理 施設 で 対応 し て い まし た が 、 処理 が 追いつか なく なっ た こと から 、 9日 と 10日 の 2日間 、 燃える ごみ など の 一般 ごみ の 収集 を 停止 し まし た 。 ,国内
11日 午前0時 50分 ごろ 、 神奈川県横須賀市田浦泉町 の 秋山 ハナ さん ( 93 ) 方 で 「 家 が 火事 に なっ て いる 」 と 娘 ( 64 ) から 通報 が あっ た 。 ,国内
県警 に よる と 、 住宅 は 全焼 し 、 焼け跡 から 2人 の 遺体 が 見つかっ た 。 ,国内
秋山 さん と 、 同居 する 娘 の 50代 夫 と 連絡 が 取れ て おら ず 、 身元 の 確認 を 進める 。 ,国内
きのう 夜 、 岐阜県 と 三重県 で 住宅 火災 が 相次い で 発生 し 、 1人 が 遺体 で 見つかり まし た 。 ,国内
消防 と 警察 に より ます と きのう 午後9時 半 ごろ 、 羽島市 正木町 の 住宅 で 「 家 から 火 が 出 て いる 」 と 近く に 住む 人 から 消防 に 通報 が あり まし た 。 ,国内
消防車 6 台 が 出て 火 は およそ 3時間後 に 消し止め られ まし た が 、 この 火事 で 木造2階建て の 建物 が 焼け 、 焼け跡 から 性別不明 の 1人 の 遺体 が 見つかり まし た 。 ,国内
この 家 に 住む 男性 と 連絡 が 取れ て おら ず 、 警察 が 身元 の 特定 を 進め て い ます 。 ,国内
また 、 この 直後 に は 尾鷲市南陽町 の 住宅 で も 火事 が あり 、 消防車 12 台 が 出て 火 は およそ 3時間後 に 消し止め られ まし た 。 ,国内
10日 午後2時 ごろ 、 群馬県神流町生利 で 建物 火災 が 発生 し た 。 近く の 山林 に 延焼 し た と み られる 。 ,国内
多野 藤岡 広域 消防 に よる と 、 午後4時 現在 で 搬送 者 は い ない 。 ,国内
同 消防 や 県 防災 ヘリ による 消火活動 が 続い て いる 。 ,国内
10日 午前 、 宇部市 で 民家 3 棟 が 全焼 する 火事 が あり まし た 。 ,国内
火事 が あっ た の は 、 宇部市西岐波 の 民家 です 。 消防 に より ます と 10日 午前8時 20分 ごろ 近隣 の 住民 から 「 爆発音 が 聞こえる 」 と 通報 が あり まし た 。 ,国内
9日 午後6時 50分 頃 、 大阪市旭区赤川 の 木造2階建て 住宅 から 出火 。 ,国内
近く の 建物 に も 延焼 し て 計 ６ 棟 を 焼き 、 住宅 の 焼け跡 から 1人 の 遺体 が 見つかっ た 。 ,国内
住宅 の 住人 と 連絡 が 取れ て おら ず 、 大阪府警 旭 署 が 遺体 の 身元確認 を 進め て いる 。 ,国内
11日 未明 、 神奈川県横須賀市 の 住宅 で 火事 が あり 、 この 家 に 住む 2人 と 連絡 が 取れ なく なっ て い ます 。 ,国内
11日 午前1時 ごろ 、 横須賀市田浦泉町 で 「 家 が 燃え て いる 」 と 消防 に 通報 が あり まし た 。 ,国内
10日 午後 、 観音寺市 の 住宅 で 火事 が あり 、 男性 1人 が 意識不明 の 状態 で 病院 に 運ば れ まし た が その後 、 死亡 し まし た 。 ,国内
10日 午後3時 すぎ 、 観音寺市 大野原町 の 住宅 から 煙 が 出 て いる と 、 近く に 住む 人 から 消防 に 通報 が あり まし た 。 ,国内
愛知県警 など に よる と 、 11日 午前11時 10分 ごろ 、 同県 愛西市 で 木造2階建て 住宅 が 燃え て いる と 119番 が あっ た 。 ,国内
1人 が 救助 さ れ 、 病院 に 搬送 さ れ た 。 この 家 に 住む 4人 の うち 3人 と 連絡 が 取れ て い ない と いう 。 ,国内
10日 の 夜 、 北谷町 で 住宅 1 棟 が 全焼 する 火事 が あり まし た 。 この 火事 による けが人 は い ませ ん でし た 。 ,国内
警察 と 消防 に より ます と 、 火事 が あっ た の は 北谷町桑江 の 住宅街 で 、 10日 午後9時 前 「 2階 から 煙 が 出 て いる 」 と 近く に 住む 人 から 警察 に 通報 が あり まし た 。 ,国内
県警 など に よる と 、 昨年12月 30日 、 熊本市西区花園 7 丁目 の 住宅 火災 で 、 1人 暮らし の 男性 ( 73 ) が 焼死 し た 。 ,国内
1月10日 午前 ５ 時半 過ぎ 、 宇都宮市駒生町 の 住宅 から 火 が 出 て いる と 近く に 住む 人 から 消防 に 通報 が あり まし た 。 ,国内
豊川市消防本部 は 、 昨年 管内 で 発生 し た 火災 が 38件 だっ た と 発表 し た 。 ,国内
```
本番データ(テストデータ・data/csv/test.csv)の例
```csvs
原稿,カテゴリー
2日 朝 、 東京・清瀬市 の 団地の 1 室 が 火事 に なり 、 1人 が 病院 に 搬送 さ れ まし た が その後 が 確認 さ れ まし た 。 ,?
アメリカ 西部 ロサンゼルス 近郊 の 山火事 で 、 警察 は 行方不明者 の 捜索 にあたる 特別 チーム を つくり 、 活動 を 本格 化 さ せ て い ます 。 ,?
アメリカ 西部 ロサンゼルス 近郊 の パシフィック・パリセーズ で 7日 に 発生 し た 山火事 は 、 非常 に 強い 風 の 影響 で 急速 に 燃え広がり 、 広い 範囲 で 延焼 が 続い て い ます 。,?
ロサンゼルス 周辺 で 相次い で いる 山火事 で 、 スポーツ界 で も NBA ＝ アメリカ プロバスケットボール や 、 NHL ＝ 北米 プロアイスホッケーリーグ の 一部 の 試合 が 延期 と なる など 、 影響 が 広がっ て い ます ,?
11日 未明 、 神戸市 の 集合住宅 で 火事 が あり 、 この 建物 の 4階 に 住む 男女 2人 が 病院 に 搬送 さ れ 意識不明 の 重体 と なっ て い ます 。 ,?
11日 午前2時 10分 ごろ 、 神戸市兵庫区湊町 の 4階建て の 集合住宅 で 「 火 が 出 て いる 」 と 近く に い た 人 など から 110番通報 が あり まし た 。 ,?
```
ストップワード(data/dic/stop.txt)の例
```
1
10分
10日
110
119番
11日
12
1つ
1人
1月10日
20分
2人
2日間
2階
3
30日
38件
3人
3時間後
40
43
4人
50代
50分
5人
6
64
7
73
7人
9000
93
9日
、
。
「
」
あっ
あらゆる
あり
ある
い
いう
いる
うち
およそ
おら
おり
から
が
きのう
けが人
ここ
こと
この
ご
ごみ
ごろ
さ
さん
し
すぎ
すでに
する
ず
その他
その後
た
ため
だ
だっ
つづり
て
で
でし
です
と
な
ない
なく
なっ
など
に
により
による
にわたって
の
は
へ
まし
ます
ませ
また
まで
み
み処理
も
もう
もたらし
もの
や
より
よる
られ
られる
れ
を
ん
午前
午前0時
午前11時
午前1時
午前8時
午後
午後2時
午後3時
午後4時
午後6時
午後9時
５
６
７
・
丁目
万
今月13日
今月3日
```

# Javaで書いてみました

## 教師データの読み込みからモデル作成まで

```java
package qiitatest;

import java.io.File;
import java.io.IOException;
import weka.classifiers.bayes.NaiveBayes;
import weka.core.Instances;
import weka.core.SerializationHelper;
import weka.core.converters.CSVLoader;
import weka.core.stopwords.WordsFromFile;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.NominalToString;
import weka.filters.unsupervised.attribute.StringToWordVector;
import java.nio.file.Files;
import java.nio.file.Path;

public class TrainClass {

    public static void main(String[] args) throws IOException, Exception {
        // CSVをArffに
        CSVLoader csvLoader = new CSVLoader();
        csvLoader.setFile(new File("data/csv/train.csv"));
        Instances csvIns = csvLoader.getDataSet();
        Files.write(Path.of("data/arff/train.arff"), csvIns.toString().getBytes());
        // テキストのAttrを設定する。この場合は冒頭(1番目)
        NominalToString n2s1 = new NominalToString();
        n2s1.setAttributeIndexes("1"); // setInputFormatより先に設定すること
        n2s1.setInputFormat(csvIns);
        Instances train_string_instances = Filter.useFilter(csvIns, n2s1);
        Files.write(Path.of("data/arff/train_string.arff"), train_string_instances.toString().getBytes());
        // 単語ベクトルにする。さらにストップワードも定義
        StringToWordVector s2wv = new StringToWordVector();
        s2wv.setInputFormat(train_string_instances);
        WordsFromFile stopwords = new WordsFromFile();
        stopwords.setStopwords(new File("data/dic/stop.txt"));
        s2wv.setStopwordsHandler(stopwords);
        s2wv.setDictionaryFileToSaveTo(new File("data/dic/arff.txt"));
        Instances train_string_wv_instances = Filter.useFilter(train_string_instances, s2wv);
        Files.write(Path.of("data/arff/train_string_wv.arff"), train_string_wv_instances.toString().getBytes());
        // モデル作成
        NaiveBayes nb = new NaiveBayes();
        train_string_wv_instances.setClassIndex(0);
        nb.buildClassifier(train_string_wv_instances);
        SerializationHelper.write("data/model/fire.model", nb);
        System.out.println(nb.toString());
    }
}
```

上記コードで、data/arff の中に、CSVから変換したarffが3つ保存されます。
いちいち保存する必要はないのですが、内容を見ると、処理の内容が確認できるはずです。

また、モデルは data/model の中に fire.model という名前で保存されます。
今回は単純ベイズ法を使いましたが(NaiveBayes)、他のアルゴリズムも使用できます。

## 実際の分類(本番・テスト)

```java
package qiitatest;

import java.io.File;
import java.io.IOException;
import weka.classifiers.Classifier;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.SerializationHelper;
import weka.core.converters.CSVLoader;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.FixedDictionaryStringToWordVector;
import weka.filters.unsupervised.attribute.NominalToString;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import weka.core.converters.ArffLoader;

public class TestClass {

    public static void main(String[] args) throws IOException, Exception {
        // テストデータをCSVから作る
        CSVLoader csvLoader = new CSVLoader();
        csvLoader.setFile(new File("data/csv/test.csv"));
        // 検証用にテストデータを行ごとのリストにしておく
        List<String> readLines = Files.readAllLines(Path.of("data/csv/test.csv"));
        Instances testCsvIns = csvLoader.getDataSet();
        Files.write(Path.of("data/arff/test_csv.arff"), testCsvIns.toString().getBytes());
        // 「カテゴリー」が ? なので、モデル作成時に作ったARFFからフィールド情報をもらう
        ArffLoader af = new ArffLoader();
        af.setFile(new File("data/arff/train_string.arff"));
        Instances trainStringArff = af.getDataSet();
        trainStringArff.clear(); // してもしなくてもいい
        // ここからフィールド1をStringにする
        NominalToString n2s2 = new NominalToString();
        n2s2.setAttributeIndexes("1"); // setInputFormatより先に設定すること
        n2s2.setInputFormat(trainStringArff); // カテゴリーのAttrを整えてくれる
        Instances testFilter = Filter.useFilter(testCsvIns, n2s2);
        Files.write(Path.of("data/arff/test_csv_string.arff"), testFilter.toString().getBytes());
        // テストデータを単語ベクトルにする
        // モデル作成時に作ったAttr情報と揃える必要がある
        FixedDictionaryStringToWordVector fixedDirSwInst = new FixedDictionaryStringToWordVector();
        fixedDirSwInst.setDictionaryFile(new File("data/dic/arff.txt"));
        fixedDirSwInst.setInputFormat(testFilter);
        fixedDirSwInst.setTFTransform(true);  // TF を使用
        fixedDirSwInst.setIDFTransform(true); // IDF を使用
        Instances testS2vIns = Filter.useFilter(testFilter, fixedDirSwInst);
        Files.write(Path.of("data/arff/test_csv_string_wv.arff"), testFilter.toString().getBytes());
        // モデルを使用して１行ごとに判別する
        Classifier fc = (Classifier) SerializationHelper.read("data/model/fire.model");
        testS2vIns.setClassIndex(0);
        int n = 0;
        for (Instance instance : testS2vIns) {
            double classifyInstance = fc.classifyInstance(instance);
            String value = testS2vIns.classAttribute().value((int) classifyInstance);
            System.out.print(value);
            System.out.println(":" + " -> " + readLines.get(n + 1));
            n++;
        }
    }
}
```

# 分類結果

以下のようになりました。まぁ、すべての原稿がこのように分類できるわけではないと思いますが、仕組みの説明としてご覧ください。

```
国内: -> 2日 朝 、 東京・清瀬市 の 団地の 1 室 が 火事 に なり 、 1人 が 病院 に 搬送 さ れ まし た が その後 が 確認 さ れ まし た 。 ,?
国際: -> アメリカ 西部 ロサンゼルス 近郊 の 山火事 で 、 警察 は 行方不明者 の 捜索 にあたる 特別 チーム を つくり 、 活動 を 本格 化 さ せ て い ます 。 ,?
国際: -> アメリカ 西部 ロサンゼルス 近郊 の パシフィック・パリセーズ で 7日 に 発生 し た 山火事 は 、 非常 に 強い 風 の 影響 で 急速 に 燃え広がり 、 広い 範囲 で 延焼 が 続い て い ます 。,?
国際: -> ロサンゼルス 周辺 で 相次い で いる 山火事 で 、 スポーツ界 で も NBA ＝ アメリカ プロバスケットボール や 、 NHL ＝ 北米 プロアイスホッケーリーグ の 一部 の 試合 が 延期 と なる など 、 影響 が 広がっ て い ます ,?
国内: -> 11日 未明 、 神戸市 の 集合住宅 で 火事 が あり 、 この 建物 の 4階 に 住む 男女 2人 が 病院 に 搬送 さ れ 意識不明 の 重体 と なっ て い ます 。 ,?
国内: -> 11日 午前2時 10分 ごろ 、 神戸市兵庫区湊町 の 4階建て の 集合住宅 で 「 火 が 出 て いる 」 と 近く に い た 人 など から 110番通報 が あり まし た 。 ,?
```