大量のニュースから興味関心のある話題をベイジアン分類で抽出する

[前々回](http://qiita.com/ynakayama/items/5a5166b3b7092656f77e)はニュースデータを収集するために RSS/Atom フィードを利用する話を書きました。

RSS/Atom フィードには全文配信と要約配信があり、昨今ではページビューを稼ぐため要約配信、特にリンクがリダイレクトになっているものや、本文がカラのものが多いという話をしました。

* 全文配信 … タイトル、リンク、それに記事本文全体を含むフィード
* 要約配信 … タイトル、リンク、記事の一部のみまたは本文がカラのフィード

## フィードデータをためる方法

[前回](http://qiita.com/ynakayama/items/4413a94599ae408c0c40)は一部で最近話題の [Fastladder](https://github.com/fastladder/fastladder) のセットアップ方法を紹介し、付属のクローラーを使ってサーバーのデータベースにフィードを溜めるという方法を説明しました。

いずれ別の記事で詳しく述べますが Fastladder はサーバー設置型な上、ソースコードは公開されていますので、クローラー自体を自作することも可能です。

また [fluentd](http://www.fluentd.org/) は柔軟なログ収集エンジンであり、入出力をプラガブルに実装することができ、 JSON フォーマットであらゆるデータをロギングできます。

このようなプロダクトを組み合わせることで、ログ収集ソリューションの要領で、ニュースフィードを集積することができます。

# ベイジアン分類をおこなう

さて、今回はその集積したデータから、ベイジアン分類という手法で興味嗜好に基づくニュースのピックアップをおこなう方法を説明します。ベイジアン分類自体についての理論はこれまた別の記事に譲るとして、まずはとにかくコードを書いて実際に記事を分類できることを確認したいと思います。

## 利用するライブラリとモデル

今までこのような機械学習を利用する場面では Python の [scikit-learn](http://scikit-learn.org/) を使ってきました。ただ今回はグルー言語として Ruby を使いますので Ruby で実装されたライブラリを使います。 [naivebayes](http://rubygems.org/gems/naivebayes) は 3 種類のナイーブベイズ分類を実装した gem です。このうち多変数ベルヌーイモデルというモデルを採用します。

これは簡単に言うと、ボキャブラリ V のすべての単語についてチェックし、要素の発生回数は無視して、その要素が発生したかどうかのみを扱うという手法です。

## 分類対象となるニュースデータ

今回はサンプルとして、あらかじめ執筆前日のニュースデータを任意に収集しました。元のデータは[こちら](https://github.com/ynakayama/sandbox/blob/master/ruby/text-mining/news.log.20141013_0.txt)になります。これは前述した fluentd を利用して、さまざまなニュースを JSON 形式のデータとして保存したものです。データ形式の詳細はリンク先を参照してください。ニュースのタイトル、リンク、本文などが JSON 形式になっています。

## 興味関心を示す単語を用意する

単なる実験なので今回は興味のある語彙を以下のように想定しました。
おそらくは、アジアに関するニュースに興味がある人なのでしょう。

|興味|関心度|
|:--|:--|
|北京|1|
|香港|1|
|中国|1|

また、執筆時の前日は台風が日本を横断したためこれに関するニュースがたくさん流れたのですが、こういった内容については関心がありません。関心の無い語彙は以下の通りとします。
おそらくは、日本国内や米国のニュースなどには興味が無いのでしょう。

|興味|関心度|
|:--|:--|
|台風|-1|
|日本|-1|
|米国|-1|
|大阪|-1|
|京都|-1|
|神戸|-1|

## コードを実装する

次の通りに実装します。

```rb

require 'json'          # JSON の解析用
require 'awesome_print' # 表示用
require 'naivebayes'    # ナイーブベイズ分類器
require 'MeCab'         # 形態素解析エンジン (自然言語処理用)

class NaiveBayesClassifier
  def initialize(args)
    @filename = args.shift || "json.txt" # 引数にファイル名を指定する

    # ベルヌーイモデルで分類器のインスタンスを生成
    @classifier = NaiveBayes::Classifier.new(:model => "berounoulli") 

    # 形態素解析エンジンのインスタンスを生成
    @mecab = MeCab::Tagger.new("-Ochasen")
  end

  # ユーザーの興味関心に基づいて分類器を学習させる
  def train
    # 興味がある単語
    @classifier.train("関心有り", {"北京" => 1, "香港" => 1, "中国" => 1})
    # 興味が無い単語
    @classifier.train("関心無し", {"台風" => 1, "日本" => 1, "米国" => 1, "大阪" => 1, "京都" => 1, "神戸" => 1})
  end

  # 学習結果に基づいて実際にニュースを分類する
  def classify
    classified = Array.new

    open(@filename) do |file| # ファイルを開く
      file.each_line do |line|
        key, tag, json = line.force_encoding("utf-8").strip.split("\t")
        hash = JSON.parse(json) # JSON データを hash に変換
        hits = {}
        # ニュースのタイトルから名詞を最大 10 件取り出して評価する
        pickup_nouns(hash['title']).take(10).each {|word|
          if word.length > 1 # 2 文字以上の単語のみを対象
            if word =~ /[一-龠]/ # 常用漢字のみを対象
              # 語彙群をハッシュにする
              hits.has_key?(word) ? hits[word] += 1 : hits[word] = 1
            end
          end
        }
        # 語彙群に対して分類をおこなう
        classify = @classifier.classify(hits)
        # 分類結果のスコアをハッシュに格納する
        hash['classify'] = classify
        hash['key'] = key
        classified << hash
      end
    end

    # ニュースデータの分類結果を返す
    classified
  end

  private

  # 与えられた文章から品詞が名詞の語彙のみを取り出して返す
  def pickup_nouns(string)
    node = @mecab.parseToNode(string)
    nouns = []
    while node
      if /^名詞/ =~ node.feature.force_encoding("utf-8").split(/,/)[0] then
        nouns.push(node.surface.force_encoding("utf-8"))
      end
      node = node.next
    end
    nouns
  end
end

if __FILE__ == $0
  clf = NaiveBayesClassifier.new(ARGV) # インスタンス生成
  clf.train # 学習
  result = clf.classify # 分類
  ap result # 結果を表示
end
```

上記のコードは [GitHub](https://github.com/ynakayama/sandbox/blob/master/ruby/text-mining/naivebayes.rb) に置いておきましたので、引数にニュースデータのファイル名を指定すれば実行することができます。

## 分類結果を見る

出力結果からいくつかピックアップしてみましょう。

```
    [ 11] {
              "title" => "台風 昼ごろにかけ九州に接近し上陸のおそれ - NHK",
               "link" => "http://www3.nhk.or.jp/news/html/20141013/t10015345741000.html",
        "description" => " 日本経済新聞    <b>台風 昼ごろにかけ九州に接近し上陸のおそれ</b> <b>NHK</b> 大型で強い台風１９号は、鹿児島県の薩摩地方の一部などを暴風域に巻き込みながら北上し、１３日の朝から昼ごろにかけて九州に最も接近して上陸するおそれがあります。 台風は、１４日にかけて西日本や東日本、それに東北に近づく見込みで、大雨や暴風、高波に警戒が必要 ... 台風19号、列島縦断の恐れ 勢力保ち九州上陸へ中国新聞 台風１９号、九 州上陸へ 強い勢力 鹿児島、宮崎で７人けが西日本新聞 台風１９号、九州南部に接近 上陸のおそれ毎日放送 朝日新聞 -日本経済新聞 -沖縄タイムス <b>all 647 news articles »</b> ",
            "content" => nil,
         "created_at" => "2014/10/13 00:30:20",
           "classify" => {
            "関心有り" => 0.38755980861244016,
            "関心無し" => 0.6124401913875599
        },
                "key" => "2014-10-13T00:30:20+09:00"
    },
```

上記は明らかに「台風」のニュースです。これは関心有り 0.39 に対し、関心無し 0.61 と高めのスコアが付きました。関心が無いということが反映されているようですね。

```
    [ 29] {
              "title" => "中国、人権派学者を拘束　香港デモと関連か - 47NEWS（よんななニュース）",
               "link" => "http://www.47news.jp/CN/201410/CN2014101201000323.html",
        "description" => "6 users",
            "content" => nil,
         "created_at" => "2014/10/13 01:00:30",
           "classify" => {
            "関心有り" => 0.8781695691616722,
            "関心無し" => 0.12183043083832784
        },
                "key" => "2014-10-13T01:00:30+09:00"
    },
```

上記は香港デモに関連すると思われる中国のニュースです。関心有り 0.87 に対し関心無し 0.12 ですから、アジアに強い関心があるということが反映された数値になっていることがわかるかと思います。

## まとめ

今回はとりあえず集積したニュースデータに対しベイジアン分類をおこなってみることをしました。このように、各人が興味の有る無しを単語で登録することで、ニュースの重み付けができ、結果として膨大なニュースから興味のある情報をピックアップできることがおわかりになるかと思います。

なお実際の分類では、たとえば自然言語処理ひとつ取っても例えばストップワードと呼ばれる頻出語彙を除外したり、品詞や係り受けなどを考慮したり、スムージングといって一度も登場したことのない語彙の確率を調整したりと様々なことをして補正をしなければなりません。簡単のため今回はそれらについては考慮していないことにご注意ください。