白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

ベイジアンフィルタでニュースのカテゴリ分けをしてみる

こんばんは。

ここ数日は涼しい日が続きましたが、明日からまた暑いようですね。
秋の気配は微妙でも、お店の品揃えはあっという間に秋モードです。


さて、今日はベイジアンフィルタを使って文章のカテゴリ分けをしてみようかと思います。

迷惑メールフィルタや、ブログの自動カテゴリ分けなどに利用されるような分類アルゴリズムです。
とてもシンプルなアルゴリズムですが、実用性もあるすごいやつです。

今回も言語はJavaです。

ベイズの法則


ベイジアンフィルタを作るにあたっては、ベイズの法則について知らなければなりません。


 {
P(B|A) = \frac{P(A|B)P(B)}{P(A)}
}


ここで、 P(B)はBの発生確率で、「事前確率」と呼ばれます。
また、 P(A|B)はBが発生した場合のAの発生確率で「事後確率」や「条件付き確率」と呼ばれます。
texで分数にすると文字が小さくなるのってどうすればいいの?)


ちょっとわかりづらいので、右辺の分母を左辺に持ってきてみます。

 {
P(B|A)P(A) = P(A|B)P(B)
}


ということは、

Aが発生した場合のBが発生する確率 × Aが発生する確率 = Bが発生した場合のAが発生する確率 × Bが発生する確率


左辺、右辺ともにAとBが両方発生する確率(同時確率)です。
そりゃそうだよねという感じです。
そして、それがどうしたの?という感じでもあります。


ちょっと例を出してみましょう。

いま、あなたの目の前に二つの箱A、Bがあります。箱の中身は完全に見えません。
箱には赤い球がひとつと、白い球がひとつ入っています。

Q1.
あなたはAの箱から赤い球を取り出しました。
Bの箱からも1つだけ球を取り出した場合、この球が赤色である確率はいくつでしょうか?

Q2.
あなたはAの箱とBの箱からそれぞれ1つだけ球を取り出し、まだ見ないでいます。
取り出した球のうち、少なくとも1つは赤色だと教えられた場合、Bの箱から取り出した球が赤色である確率はいくつでしょうか?


Q1の答えはもちろん1/2です。
そうですよね。Aの箱とBの箱は関係ありませんものね。

Q2の答えはどうなるでしょうか。
箱から取り出すときの条件は変わらないので1/2でしょうか。
実際にやってみてください、きっと2/3になるはずです。

ポイントはすでに結果が出ていることです。
「赤・赤」「赤・白」「白・赤」「白・白」の4つのパターンのうち、
「白・白」のパターンでは問題文に当てはまらないので除外しなくてはなりません。


ベイズの定理にあてはめてみましょう。

どちらかの球が赤である場合のBの箱の球が赤である確率
 = Bの箱の球が赤である場合のどちらかの球が赤である確率 × Bの箱の球が赤である確率 ÷ どちらかの球が赤である確率
 = 1 × 1/2 ÷ 3/4
 = 2/3

ちゃんと2/3になりましたね。

こんな風に結果から確率を簡単に求めることができるのがベイズの定理の便利なところです。

Bag-of-Words


自然言語を処理する際に特徴量をどうやって抽出するかという問題があります。
理想としては文自体をそのまま使いたいところなのですが、何に着目したらよいかがわかりません。

そこで登場するのが「Bag-of-Words」という手法です。
これはその名の通り、単語をバッグに入れてしまうという考え方です。

バッグに入れるというのはどういうことかというと、その順序を気にしないということです。
何番目にバッグの中に入れたかというのは気にしませんが、何個入っているかは気にします。

本来は単語間には何かしらの関係があり、その順序も意味理解には重要な役目を果たしますが、
Bag-of-Wordsにおいてはそれは考慮をしないというのがポイントです。
通常は名詞のみを抜き出し、出現回数を数えます。


ちょっと例を挙げてみましょう。

これを、

台風が日本の南の海上を北上しています。
台風の接近に伴い、大気の状態が不安定になっています。

こう分けて、

台風 が 日本 の 南 の 海上 を 北上 し て い ます 。
台風 の 接近 に 伴い 、 大気 の 状態 が 不安定 に なっ て い ます 。

名詞だけ抜き出すと、

台風 日本 南 海上 北上 台風 接近 大気 状態 不安定

なんだかその文章の特徴的な単語が拾えている感じがしますね。


あるカテゴリの文章に同じことをしていくと、
そのカテゴリ「らしさ」が強い単語の出現傾向が高くなっていくことは簡単に予想できます。

たとえば「天気」のカテゴリの文章では、「雨」「晴れ」「気温」などが多く出現するでしょう。
一方で「スポーツ」のカテゴリの文章では、「選手」「勝利」「ボール」などが多く出現するでしょう。

これをたくさんの文章に対して行い、
一番「らしい」カテゴリに分類してしまいましょうというのが今回の手法です。

ベイズの定理に適用


分類の方法は決まりましたが、
これをどうやってベイズの定理に適用するのかがまだつながっていないので見ていきましょう。

ある文章である確率をP(text)、あるカテゴリである確率をP(cate)としましょう。
ある文章があるカテゴリである確率は、P(cate|text)になりますね。
これをベイズの定理に当てはめてみると、


 {
P(cate|text) = \frac{P(text|cate)P(cate)}{P(text)}
}

となります。


P(cate)は対象のカテゴリである確率です。これは「対象のカテゴリの件数/全件数」で表すことができます。


一方で、P(text|cate)は対象のカテゴリにおける対象の文章である確率です。
これは簡単に計算することはできません。

ところで、対象のカテゴリにおける対象の単語である確率、P(word|cate)はどうでしょう。
バッグの中に入っているすべての単語のうち、ある単語である確率は求めることができそうです。
つまり、「対象の単語/対象のカテゴリの全単語」ということです。
これを文中のすべての単語について計算し、その同時確率を求めればP(text|cate)を計算することができます。

 {
P(text|cate) = P(word1|cate) × P(word2|cate) × P(word3|cate) ...
}


また、P(text)はある文章である確率ですが、
これはある文章のカテゴリを分類するにあたって、どのカテゴリに対しても変わらない値になります。
となると、(必ず正の数となるため)大小を比較するにあたっては特に気にする必要はありません。

アンダーフロー問題とゼロ頻度問題


ベイジアンフィルタを実装するための理論は出そろったのですが、
実装にあたって2つほど問題が発生します。

ひとつめは「アンダーフロー問題」です。

P(text|cate)を求めるにあたって、P(word|cate)をすべて乗算すればよいことは分かりましたが、
文が長くなるにつれて乗算の数は増え、カテゴリに含まれる単語の数が増えるにつれてP(word|cate)はとても小さな値になります。
Javaのdouble型だと10のマイナス320乗くらいでアンダーフローしてしまうため、このままでは計算できません。

上でも書きましたが、大小のみを比較することができればよいので、対数を取っても構いません。
なので、積を対数の和に変換してしまいます。


ふたつめは「ゼロ頻度問題」です。

対象のカテゴリにおける対象の単語である確率、P(word|cate)を求めるにあたって、
今まで出現したことのない単語が出現した場合、その値はどうなるでしょうか。
当然0になりますよね。

すると、「何か×何か×何か×0×何か×何か」みたいになって、
他がどうであろうが、確率が0に固定されてしまいます。

これを避けるためには加算スムーシングという手段を使います。
これは簡単に言ってしまえば、「全部の単語がとりあえず1回は出てきたことにする」ということです。
すると、未知の単語でも出現回数は1になるので、0にはなりません。

実装


今回は形態素分析にはKuromojiを利用しました。
KuromojiにはAtilikaとLuceneの2種類がありますが、ipadic-neologdという拡張辞書を使いたかったので、
Lucene版を利用しています。

また、Bag-of-Wordsのイメージをそのまま実装したかったため、
Eclipse CollectionsのMutableBagを利用しています。
Apache CommonsのHashBag等でも同じように実装できると思います。

StreamHelper.whileStreamについては
WhileStreamってないの? - 白猫のメモ帳
を参照。

Mavenを利用する場合にはpom.xmlに以下を追加します。

    <repositories>
        <repository>
            <id>codelibs.org</id>
            <name>CodeLibs Repository</name>
            <url>http://maven.codelibs.org/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.codelibs</groupId>
            <artifactId>lucene-analyzers-kuromoji-ipadic-neologd</artifactId>
            <version>5.5.0-20160411</version>
        </dependency>
        
        <dependency>
            <groupId>org.eclipse.collections</groupId>
            <artifactId>eclipse-collections</artifactId>
            <version>7.1.0</version>
        </dependency>
    </dependencies>


実装はそんなに長くない。

/**
 * ナイーブベイズの実装
 */
public class NaiveBayes {
    
    /** カテゴリの生起確率 */
    private final MutableBag<String> categoryBag = Bags.mutable.empty();
    
    /** カテゴリごとの単語の生起確率 */
    private final MutableMap<String, MutableBag<String>> categorizedWordBag = Maps.mutable.empty();
    
    /** 単語の数 */
    private final MutableSet<String> wordSet = Sets.mutable.empty();
    
    /**
     * 学習
     * 
     * @param str
     * @param category  カテゴリ
     */
    public void lern(String str, String category) {
        
        // カテゴリを追加
        this.addCategory(category);
        
        // 単語を追加
        this.toWords(str).forEach(word -> this.addWord(category, word));
    }
    
    /**
     * カテゴリを追加
     */
    private void addCategory(String category) {
        categoryBag.add(category);
    }
    
    /**
     * カテゴリに単語を追加
     */
    private void addWord(String category, String word) {
        categorizedWordBag.getIfAbsentPutWithKey(category, c -> Bags.mutable.empty()).add(word);
        wordSet.add(word);
    }
    
    /**
     * 評価
     * 
     * <pre>
     * P(cate|text) = P(text|cate)P(cate) / P(text)
     * 比較する際にはP(text)で割る必要はない。
     * </pre>
     * 
     * @param str
     * @return カテゴリ
     */
    public String predict(String str) {
        List<String> words = this.toWords(str);
        return this.categoryBag.toMapOfItemToCount().keySet().stream().map(cate -> Tuples.pair(cate, this.score(cate, words)))
                                                             .max(Comparator.comparing(p -> p.getTwo()))
                                                             .get().getOne();
    }
    
    /**
     * 評価(順位)
     * 
     * @param str
     * @return カテゴリ
     */
    public List<Pair<String, Double>> ranking(String str) {
        List<String> words = this.toWords(str);
        return this.softMax(this.categoryBag.toMapOfItemToCount().keySet().stream().map(cate -> Tuples.pair(cate, this.score(cate, words)))
                                                                 .sorted(Comparator.<Pair<String, Double>, Double>comparing(p -> p.getTwo()).reversed())
                                                                 .collect(Collectors.toList()));
    }
    
    /**
     * 確率 P(text|cate)P(cate)
     * 
     * <pre>
     * P(word|cate) はごく小さな値のため、アンダーフローを起こさないように乗算から対数の加算に変換。
     * </pre>
     * 
     * @param category  カテゴリ
     * @param words
     */
    private double score(String category, List<String> words) {
        
        // P(text|cate)⇒この文書がこのカテゴリである確率
        double wordProb = words.stream().mapToDouble(word -> Math.log(this.wordProb(category, word))).sum();
        
        // P(cate)⇒このカテゴリである確率
        double priorProb = Math.log(this.priorProb(category));
        
        // P(text|cate)P(cate)
        return wordProb + priorProb;
    }
    
    /**
     * カテゴリの発生率 P(cate) 
     * 
     * <pre>
     * つまり、全体の件数のうちのこのカテゴリである確率。
     * </pre>
     */
    private double priorProb(String category){
        return (double) categoryBag.occurrencesOf(category) / categoryBag.size();
    }
    
    /**
     * カテゴリに単語が含まれる条件付き確率 P(word|cate) 
     * 
     * <pre>
     * (あるカテゴリでの)ある単語の出現回数 / 全単語の出現回数合計
     * 未知の単語のゼロ頻度問題を回避するために加算スムージングを行う。(分子に1、分母に単語数を足す)
     * </pre>
     */
    private double wordProb(String category, String word) {
        MutableBag<String> wordBag = categorizedWordBag.get(category);
        return ((double) wordBag.occurrencesOf(word) + 1) / (wordBag.size() + wordSet.size());
    }
    
    /**
     * SoftMax関数
     */
    private List<Pair<String, Double>> softMax(List<Pair<String, Double>> ranking) {
        double sum = ranking.stream().mapToDouble(p -> Math.exp(p.getTwo())).sum();
        return ranking.stream().map(p -> Tuples.pair(p.getOne(), Math.exp(p.getTwo()) / sum))
                               .collect(Collectors.toList());
    }
    
    /**
     * 形態素分解(名詞のみ)
     */
    private List<String> toWords(String src) {
        
        try (JapaneseTokenizer jt = new JapaneseTokenizer(null, false, JapaneseTokenizer.Mode.NORMAL)) {
            
            jt.setReader(new StringReader(src));
            jt.reset();
            
            return StreamHelper.whileStream(jt, StreamHelper.throwingPredicate(j -> j.incrementToken()), this::toToken)
                               .filter(p -> "名詞".equals(p.getTwo()))
                               .map(p -> p.getOne())
                               .collect(Collectors.toList());
            
        } catch (IOException e) {
            return Collections.EMPTY_LIST;
        }
    }
    
    /**
     * トークンに変換
     */
    private Pair<String, String> toToken(JapaneseTokenizer jt) {
        
        CharTermAttribute ct = jt.getAttribute(CharTermAttribute.class);
        PartOfSpeechAttribute pos = jt.getAttribute(PartOfSpeechAttribute.class);
        
        return Tuples.pair(ct.toString(), pos.getPartOfSpeech().split("-")[0]);
    }
}


評価メソッドにはpredictとrankingを作りましたが、

・predictはもっとも可能性の高いカテゴリを返します
・rankingはそれぞれのカテゴリである確率を返します

試す


で、試してみたいのですが、自分でカテゴライズした文章をたくさん集めてくるのは辛いです。
ので、YahooニュースのRSSからタイトルとカテゴリを取ってくることにしました。

pom.xmlのdependenciesにさらに以下を追加します。

        <dependency>
            <groupId>rome</groupId>
            <artifactId>rome</artifactId>
            <version>1.0</version>
        </dependency>
        
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.17</version>
        </dependency>


どこかにこんなのをつくります。
エラーハンドリング面倒だったので、rss.yamlは最初に空ファイル作ってくださいまし。

public static void collect() throws Exception {

    Yaml yaml = new Yaml();
    Map<String, String> map;

    try (BufferedReader br = Files.newBufferedReader(Paths.get("C:\\Users\\shironeko\\Desktop\\rss.yaml"))) {

        map = Optional.ofNullable(yaml.loadAs(br, Map.class)).orElse(new HashMap<>());

        List<Twin<String>> list = new ArrayList<>();
        list.add(Tuples.twin("http://headlines.yahoo.co.jp/rss/all-bus.xml", "経済"));
        list.add(Tuples.twin("http://headlines.yahoo.co.jp/rss/all-c_ent.xml", "エンタメ"));
        list.add(Tuples.twin("http://headlines.yahoo.co.jp/rss/all-c_spo.xml", "スポーツ"));
        list.add(Tuples.twin("http://headlines.yahoo.co.jp/rss/all-c_sci.xml", "IT・科学"));
        list.add(Tuples.twin("http://headlines.yahoo.co.jp/rss/all-c_life.xml", "ライフ"));

        for (Twin<String> master : list) {

            SyndFeed feed = new SyndFeedInput().build(new XmlReader(new URL(master.getOne())));
            feed.getEntries().forEach(e -> {
                SyndEntry se = (SyndEntry)e;
                map.putIfAbsent(se.getTitle().replaceAll("(.*?)", ""), master.getTwo());
            });
        }
    }

    System.out.println(map.size());

    try (BufferedWriter bw = Files.newBufferedWriter(Paths.get("C:\\Users\\shironeko\\Desktop\\rss.yaml"))) {
        bw.write(yaml.dump(map));
    }
}

実行するたびにYAMLファイルに追記していきます。
スケジューラとかに入れるといっぱいとれるかもね。

で、こんな感じで遊んでみます。
読み込んだうちの後ろ10件を判定の対象とし、残りは学習データです。

public static void predict() throws Exception {

    NaiveBayes nb = new NaiveBayes();

    // よみこむ
    Yaml yaml = new Yaml();
    Map<String, String> map = yaml.loadAs(Files.newBufferedReader(Paths.get("C:\\Users\\shironeko\\Desktop\\rss.yaml")), Map.class);

    // まぜる
    List<Entry<String, String>> list = new ArrayList<>(map.entrySet());
    Collections.shuffle(list);

    // まなぶ
    List<Entry<String, String>> learningData = new ArrayList<>(list.subList(0, list.size() - 10));
    Collections.sort(learningData, Comparator.comparing(e -> e.getValue()));
    learningData.forEach(e -> System.out.println(e.getValue() + " " + e.getKey()));
    learningData.forEach(e -> nb.lern(e.getKey(), e.getValue()));

    // ためす
    System.out.println("");
    List<Entry<String, String>> testData = new ArrayList<>(list.subList(list.size() - 10, list.size()));
    testData.forEach(e -> System.out.println((nb.predict(e.getKey()).equals(e.getValue()) ? "○" : "×") + nb.ranking(e.getKey()) + " " + e.getKey()));
}

 

結果


とりあえず100件集めてみます。

×[エンタメ:0.3204679985032405, 経済:0.30911445236458013, ライフ:0.1388558110142599, スポーツ:0.12647387218325035, IT・科学:0.10508786593466922] パソコン工房/TSUKUMO、Windows 8.1以前のメーカー製PC下取りでBTO PCを5,000円引きするキャンペーン
×[エンタメ:0.30106146746930584, スポーツ:0.2561951026567542, IT・科学:0.15695441234552157, 経済:0.14363834020995578, ライフ:0.14215067731846268] 野良猫がお客さんの後について来店 いつしか看板猫に
×[IT・科学:0.3779400513995687, エンタメ:0.16788738178672913, 経済:0.16077791914565642, ライフ:0.1540648936759163, スポーツ:0.13932975399212938] 『超銀河船団∞ -INFINITY-』のリリースが延期に、8月下旬から2016年に変更
○[経済:0.4263712778755095, IT・科学:0.1630342121784406, エンタメ:0.14858651215852664, ライフ:0.13766104484776373, スポーツ:0.12434695293975954] トヨタ、レクサス専用の部品センターを開所…米アトランタ
×[スポーツ:0.5764779713722937, ライフ:0.21172541506713133, IT・科学:0.09616246989954291, 経済:0.07606339810811541, エンタメ:0.03957074555291659] お盆期間中の「バスタ新宿」、1日平均利用者数3万6000人
○[スポーツ:0.3678477316434514, 経済:0.3093562508143622, IT・科学:0.11267843130882116, エンタメ:0.10806683729060819, ライフ:0.1020507489427571] 石川遼、米ツアーでの飛躍を見すえた半年間
○[スポーツ:0.9539622451049281, IT・科学:0.020216234419709555, エンタメ:0.008980394254970103, 経済:0.00860010493972592, ライフ:0.008241021280666276] 芦毛初のダービー馬ウィナーズサークル死す けい養先で老衰
×[ライフ:0.7409093198053467, IT・科学:0.15039365858559753, 経済:0.04532634688074401, エンタメ:0.03528567161916263, スポーツ:0.02808500310914912] J1第2S第10節のノミネートゴール発表…G大阪FW長沢の高難易度ボレーなど
○[IT・科学:0.8429683411934602, 経済:0.08134613733449596, エンタメ:0.028178841659984164, ライフ:0.02488998331673797, スポーツ:0.02261669649532165] SCSK、IoTデータの収集・分析プラットフォーム「Red Hat JBoss BRMS on Azure」を提供開始
○[IT・科学:0.4830600740573177, スポーツ:0.19056054951976423, エンタメ:0.11290607147441922, 経済:0.10786574448819343, ライフ:0.1056075604603054] EMCジャパン、クラウドサービス事業者との協業を強化する新プログラム

何回かやってみると、正答率は平均で5割程度ですね。
0.95とかいうのがありますが、同じ内容がちょっと違う文章でタイトルになっていることがあるみたいです。


300件集まりました。

○[スポーツ:0.7330562019592453, エンタメ:0.12821141203721184, 経済:0.11576513862360815, ライフ:0.017410758067615762, IT・科学:0.005556489312319009] 阪神D2位・坂本、故郷の応援を背に1軍で奮闘中!
×[ライフ:0.3448310724122439, スポーツ:0.24890123107277065, IT・科学:0.161355677699115, 経済:0.14214541472890885, エンタメ:0.10276660408696162] 「VISUAL JAPAN SUMMIT」第4弾出演アーティスト29組発表!)
×[経済:0.4070859408333333, エンタメ:0.217558111945667, IT・科学:0.17845710052538064, スポーツ:0.1456568429681256, ライフ:0.051242003727493525] 「聲の形」公開記念で京アニ特集、映画「けいおん!」など5夜連続で劇場上映
○[エンタメ:0.8986573927304412, 経済:0.0402167266895944, ライフ:0.023401432711473123, IT・科学:0.021841402613504396, スポーツ:0.015883045254986895] 高畑充希 松田聖子&松たか子と感激のCM共演「こんなところに来てしまった」
○[経済:0.3333326326952095, ライフ:0.24154122451019366, IT・科学:0.18643038714548488, エンタメ:0.13109496153682912, スポーツ:0.10760079411228282] レクサス、HS、RC、RC F、NX、LSを一部改良
×[エンタメ:0.4056946135618596, 経済:0.2670009333244538, ライフ:0.13922257275527852, スポーツ:0.11663651581835531, IT・科学:0.07144536454005283] フォードが“完全な”自動運転車の開発を本格化へ
○[エンタメ:0.9286921287658515, IT・科学:0.03018730955682386, 経済:0.026987058072000136, ライフ:0.009777751130953534, スポーツ:0.004355752474370977] (朝鮮日報日本語版) CAに変身したキム・ハヌルのスチール公開=『空港へ行く道』
○[エンタメ:0.23306216061908308, スポーツ:0.20776849915572124, IT・科学:0.20202017507972994, 経済:0.19437416424166692, ライフ:0.16277500090379884] 大森靖子×生ハムと焼うどん、新宿LOFTでツーマンライブ
○[エンタメ:0.4019068001760029, ライフ:0.20688405832067963, 経済:0.19838103681171257, IT・科学:0.1061674602884122, スポーツ:0.08666064440319265] まゆゆが舞妓さんに! メロメロの豊川悦司「ファンクラブに入りたい」
×[IT・科学:0.35865056560810143, ライフ:0.19213365612381617, 経済:0.1650964458346106, エンタメ:0.1537141733628263, スポーツ:0.1304051590706456] 高畑容疑者、抑えられなかった“性衝動”と慢心 精神科医「錯覚し続けてきた」

6割くらいになったかな・・・?
そもそも「ライフ」というジャンルがなんなのかよくわかりません。


500件。

×[IT・科学:0.9013845330964335, 経済:0.03400325236805277, スポーツ:0.03219217874663204, エンタメ:0.02935389758737487, ライフ:0.0030661382015067327] ソニー、柄変えられる腕時計の第2弾 ネットで予約購入者集まれば販売
○[経済:0.9965547325892609, ライフ:0.0016023622239106604, エンタメ:8.090766634554242E-4, IT・科学:5.64735562479902E-4, スポーツ:4.6909296089309177E-4] トヨタ、クラウン誕生60周年で復元した初代「クラウン」など52台を披露
○[エンタメ:0.9894459457325713, 経済:0.005788279552111944, IT・科学:0.002681993672488239, ライフ:0.0017675129255949038, スポーツ:3.1626811723363575E-4] (朝鮮日報日本語版) 興行成績:ハ・ジョンウ主演『トンネル』、公開から18日で600万
○[経済:0.32778761304914283, IT・科学:0.2633426544379308, スポーツ:0.18197028739836146, ライフ:0.13917905979007886, エンタメ:0.08772038532448602] BMWブランドのPHV販売、西欧で好調 7月
○[IT・科学:0.962374298687313, 経済:0.029974745083738253, ライフ:0.0043736197693733, スポーツ:0.002618512543869786, エンタメ:6.588239157056864E-4] au、3つの専用特典を用意した無料の会員制プログラム「au STAR」開始
○[経済:0.47297056232196044, スポーツ:0.46517089271588874, ライフ:0.04332798242397985, エンタメ:0.010312420748367522, IT・科学:0.008218141789803526] 【ジャパントラックショー16】UDトラックス、クオン 4×2トラクターなどを展示
○[経済:0.44698042034080876, スポーツ:0.23803506516257833, エンタメ:0.14469885123297677, ライフ:0.12091523428095102, IT・科学:0.04937042898268509] 大手銀行、5カ月ぶりに固定型住宅ローン金利を引き上げ
○[IT・科学:0.4264170829553563, スポーツ:0.314276766771388, 経済:0.2130308974360502, ライフ:0.03512767915662222, エンタメ:0.01114757368058331] Fitbit、有酸素運動能力も測定できる「Charge 2」と水泳対応の「Flex 2」
×[ライフ:0.315127842111263, 経済:0.27925743124752506, スポーツ:0.16788685580304288, IT・科学:0.1337037440225935, エンタメ:0.10402412681557557] シュコダの新SUV コディアック、「美しい」と自信…9月1日発表へ
×[ライフ:0.4348922676934627, スポーツ:0.25435547611982223, 経済:0.20328403879192916, IT・科学:0.06571591301663034, エンタメ:0.041752304378155566] 【MLB】3Aでストライク率70% ロッテ戦力外の中後、11戦無失点で防御率0.00継続

7割は行ってないくらい?


800件。

×[IT・科学:0.5527193174520223, ライフ:0.18813148437926536, エンタメ:0.09202280017593435, 経済:0.08654134288509936, スポーツ:0.08058505510767869] Facebookはメディア企業ではない!マーク・ザッカーバーグ断言
×[IT・科学:0.606758782884436, 経済:0.1910729517004838, ライフ:0.10362207617068764, エンタメ:0.0703406208373598, スポーツ:0.028205568407032845] 次世代都市交通システム、自動走行でバスの停車位置ピタリ-車いすでもスムーズに乗り降り
○[IT・科学:0.9583026983831147, ライフ:0.016218193781154758, スポーツ:0.012938835722527329, エンタメ:0.007833810813636207, 経済:0.004706461299567032] カスペルスキー、自社ウイルス対策ソフトの脆弱性に対処--Cisco Talosの報告受け
○[エンタメ:0.9987661276094787, ライフ:4.367056103785426E-4, IT・科学:3.018846233423588E-4, 経済:2.8439915977932075E-4, スポーツ:2.108829970210718E-4] (朝鮮日報日本語版) 元少女時代ジェシカ、中傷ネットユーザーを警察に告訴
○[経済:0.9836917462852619, スポーツ:0.005962261441480325, ライフ:0.00523362038196213, エンタメ:0.005022238632575722, IT・科学:9.013325871990902E-5] 都市ガスの7月の販売量3・9%減 気温上昇で3月ぶりに減少
○[スポーツ:0.5462798784968125, 経済:0.3322891575627247, エンタメ:0.09495419034816258, IT・科学:0.016827552175913297, ライフ:0.009649221416386954] 石川遼、米ツアーでの飛躍を見すえた半年間
○[スポーツ:0.9862291249494178, IT・科学:0.004547973403801789, 経済:0.004280549657884559, ライフ:0.0029289534603115027, エンタメ:0.0020133985285843707] 臨戦態勢で臨む日本代表、アジア最終予選初戦 問われる指揮官のチームマネジメント
○[IT・科学:0.9996668926529206, 経済:1.9395994376741074E-4, エンタメ:8.071061293105649E-5, ライフ:4.1773422662732675E-5, スポーツ:1.6663367718199768E-5] 文字盤からバンドまで全部電子ペーパーの腕時計--ソニーの「FES Watch」に新モデル
×[経済:0.5677311238636722, エンタメ:0.1769238542364387, ライフ:0.15546256521469673, IT・科学:0.05910077691756894, スポーツ:0.04078167976762339] 天下統一を狙え!1000人以上の武将が登場する戦国ゲーム―注目のiPhoneアプリ3選
○[エンタメ:0.7723832779247336, スポーツ:0.08454772743387433, ライフ:0.04934565547474798, IT・科学:0.04832488494809255, 経済:0.04539845421855166] カンニング竹山、高畑淳子の芸能活動は難しい?「被害者が傷つくんじゃないかとなると……」)

7割超えるかくらいですかね。
このあたりからはそんなに変わらないのかも知れません。

「次世代都市交通システム」とかは私でも「IT・科学」に分類したくなりますね。


コーパスが増えてくると語彙もどんどん増えてきてしまうので、
単純にひたすら線形に賢くなってはいかないようです。

でも、こんなに簡単な仕組みでそこそこの精度がでるのはなかなか素晴らしいですね。