ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

poem_detector: Qiitaのクズ記事を判定する機械学習モデル

記事の良し悪しを判定するのは、文書分類の典型的なタスクです。イイネが記事の良し悪しを意味しないなら、アノテーション基準を制定する方法でモデルを構築すればそれなりのものができそうなので、試してみましょう。

パイプライン

  1. 良い記事と悪い記事の基準を考える。
  2. その基準を元に、良い記事URLと悪い記事URLを手動で収集。
  3. URLから本文テキストのみを抽出。
  4. 本文テキストだけを用いてモデリング
  5. モデルを他の記事へ適用。

注意: スクレイピングでQiitaに負荷をかけないに気をつけましょう。

パイプラインをモジュール化したgithub上のプロジェクト

上記パイプラインを誰でも簡単に実行できる形式にしたので、以下のgithubプロジェクトで公開しておきます。

https://github.com/sugiyamath/poem_detector

実行方法

訓練済みモデルを使いたい場合はプロジェクト内のREADME.mdを参考にしてください。 イチから自分のモデルを作ってみたい方は以下。

1. 良い記事と悪い記事のURLリストを手動で定義

プロジェクト内に bad_articles.txtgood_articles.txt があります。それぞれに悪い記事と良い記事のURLを貼ってください。URLは多いほどよいです。

2. コンテンツ抽出

python get_content.py を実行すると、content.csv というファイルが出力されます。

3. 前処理 & 訓練

python model.py を実行すると、content.csv を使って訓練が行われます。訓練によって、vec.pklmodel.pkl が出力されます。これがモデルです。

4. 交差検証

モデルをAUCで交差検証をするために、python eval.py を実行してください。最初に出力される配列はRandomForestの交差検証結果、次のものはLogisticRegressionです。目安として、0.8以上の値が出れば汎化性能を持っている可能性があります。汎化性能は、前処理・特徴量・モデル・データなどに依存します。URLリストの定義次第で性能が変わります。

5. モデルを利用する

predictor.py を使えばモデルを実際の予測に使えます。predictor.pyの中身は以下です。

from model import predict, load
from get_content import get_content

class Predictor:

    def __init__(self):
        self.clf, self.vec = load()

    def predict(self, path, is_url=True):
        if is_url:
            return predict(self.clf, self.vec, [get_content(path)])
        else:
            with open(path) as f:
                return predict(self.clf, self.vec, [f.read()])

    def predict_list(self, path_list, is_url=True):
        if is_url:
            return predict(self.clf, self.vec, [get_content(path) for path in path_list])
        else:
            for path in path_list:
                data = []
                with open(path) as f:
                    data.append(f.read())
                return predict(self.clf, self.vec, data)


if __name__ == "__main__":
    predictor = Predictor()
    print(predictor.predict("https://qiita.com/taku910/items/7e52f1e58d0ea6e7859c"))
    print(predictor.predict("https://qiita.com/Yuta_Yamamoto/items/fbeb7b31173b3e787fc2"))
    print(predictor.predict("https://qiita.com/riversun/items/29d5264480dd06c7b9fb"))
    print(predictor.predict("https://qiita.com/_EnumHack/items/3d7d50c43523c71ab307"))
    print(predictor.predict("https://qiita.com/tani_AI_Academy/items/3edc5effeb386ae3caa9"))
    print(predictor.predict("https://qiita.com/charmston/items/df31a419a4e57ebe86ba"))
    print(predictor.predict("https://qiita.com/rana_kualu/items/afe544b0f5680e81fabc"))
    print(predictor.predict("https://qiita.com/qiitadaisuki/items/2160a390ce91283707a1"))
    print(predictor.predict("https://qiita.com/Kosuke-Szk/items/4b74b5cce84f423b7125"))
    print(predictor.predict("https://qiita.com/Y_F_Acoustics/items/6742a0a8ad6b37a0f4f5"))
    print(predictor.predict("https://qiita.com/ozikot/items/f7e5c346e631de067efb"))
    print(predictor.predict("https://qiita.com/kurogelee/items/1c081a5a7e209e81921c"))

使い方はコードを見てのとおりです。要するに、Predictorクラスのpredictを呼べば、指定したURLを判定します。sklearnのpredict_probaを使っているので、出力は以下のようになります。

[[0.1, 0.9]]

0.1は「悪い記事である確率」で、0.9は「良い記事である確率」です。

モデルの詳細

このモデルは文字ベースBowを使ったランダムフォレストです。

なぜこのモデルを使ったかというと、

  1. ユーザー属性を使いたくない。
  2. いいね数を使いたくない。

の二点です。要するに、バイアスを避けるためにコンテンツだけを評価するモデルが欲しかったわけです。

単語ベースBoWとLogisticRegressionを使ったモデルはよく見かけますが、これが機能する理由は「単語の出現の有無を重みで評価する」ようなモデルだからです。

これに対し、文字ベースBoW + RandomForestは、「文字の出現の組合せを条件分岐的に評価する」ようなモデルとなります。

なぜ文字ベースを採用するのかというと、単語ベースの場合は特徴量ベクトルが大きくなりすぎてしまうのと、データ数が多くないと精度が上がらない可能性があったためです。文字ベースならば、文字の組合せであるため、遥かに少ない特徴量ベクトルになります。

訓練済みモデルの交差検証の結果を確かめるため、以下を実行します。

git clone https://github.com/sugiyamath/poem_detector/
cd poem_detector
python eval.py
[0.8625     0.80972222 0.86111111 0.73611111 0.87777778]
[0.675      0.46666667 0.82777778 0.63055556 0.63055556]

RandomForestはLogisticRegressionよりも高い精度が出ており、文字ベースに適していることが推測できます。しかし、評価データが少ないこともあり、文字ベースBoW+RandomForestという手法に確証が持ててはいません。

組合せを評価するのであれば、むしろLSTMやCNNのほうがよいかもしれません。試してみてはどうでしょう。

教師データが重要

結局、モデリングがどうであれ、記事の良し悪しの分類の本質は教師データの用意です。一貫したアノテーション基準のもとでURLを用意し、URLを大量に用意すれば精度は高まります。

これは、「いいね数」「リツイート数」「contribute数」を使った方法とは本質的に異なっています。いいね数を使ったモデルは、結局「いいね数の予測」でしかありませんから、記事の良し悪しについて何も言及していることにはなりません。

一方、一貫したアノテーション基準を元に教師データが用意できれば、「良い記事とは○○である」と定義された上で教師データを用意できるため、定義を柔軟に変更することが可能です。これは、良い記事の定義方法について柔軟性を持っていると言えます。

なにより、本文だけを使ってバイアスを極力避ければ、出力の良し悪しは教師ラベルに依存することになるため、「良い教師ラベルを準備すること」に専念できます。End2Endの良さはこういったところにもあるのかもしれません。

リンク

https://github.com/sugiyamath/poem_detector/