人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

ナイーブベイズを用いたブログ記事の自動分類

カイ二乗値を用いた特徴選択(2010/6/25)の続きです。今まで使ってきた20 Newsgroupsというデータは英語文書でかつ元ネタがよく分からずあまり面白くなかったので、今回はこのブログ(人工知能に関する断想録)の記事を分類してみます。このブログの各記事には私の判断でカテゴリをつけています。たとえば、この記事は[機械学習][自然言語処理]です。カテゴリのリストはこのブログの左メニューにあります。この前、少し整理したので全部で18のカテゴリがあります。新しい記事を書いたとき自動でカテゴリを割り振ることはできるのでしょうか?

(注)プログラミング言語はPythonを使っています。シリーズもので以前作ったコードを再利用してるので検索で飛んできた人はナイーブベイズを用いたテキスト分類(2010/6/13)から順に読んでください。

はてなダイアリーデータのダウンロードと整形

まず、はてなダイアリーのデータを全部ダウンロードしてみます。これは、自分のブログなら管理>データ管理>ブログのエクスポート>Movable Type形式からダウンロードできます。Movable Type形式以外にはてな日記データ形式、CSV形式でもエクスポートできますが、Movable Type形式が一番整形しやすそうでした。私のブログ(人工知能に関する断想録)のエクスポートデータはaidiary.txtです。けっこう長年に渡って書きためてきた感じがしますが、たった3.6MB程度とはちょっとショックかも。このファイルをナイーブベイズの入力ファイル形式に変換します。すなわち、下のような形式のファイルです。

カテゴリ 単語:カウント 単語:カウント ...     <- 文書1
カテゴリ 単語:カウント 単語:カウント ...     <- 文書2
...

整形のポイントは、

  • Movable Typeのファイルからエントリごとにタイトル、カテゴリ、日付、本文を抽出する
  • ソースコード部分は意味のある単語を含まないので無視する
  • タイトルと本文から単語(名詞と固有名詞のみ)を形態素解析で抽出する
  • 形態素解析にはMeCab Pythonを用いる
  • エントリごとに単語の出現頻度をカウントする
  • エントリごとにカテゴリと単語:カウントをファイルに出力する
  • 単語はカウントの降順でソートする

参考までに変換プログラムを掲載します。この変換プログラムをaidiary.txtと同じフォルダでプログラムを実行するとblog.txtという変換されたファイルができます。ただし、このプログラムを動かすには後述する形態素解析モジュールMeCab Pythonの導入が必要です

#coding:utf-8
import codecs
import re
import sys
from collections import defaultdict

# MeCab Pythonのインストールが必要
import MeCab

"""
はてなダイアリーのブログデータの前処理
HTMLタグは除去
プログラムコードブロックは除去
"""

# ブログデータ(Movable Type形式)
# 自分のブログなら管理メニューから全文ダウンロード可
FILE = "aidiary.txt"
OUTPUT = "blog.txt"

def analysis(tagger, text):
    """textをMeCabで形態素解析して単語のリストを返す"""
    node = tagger.parseToNode(text.encode("utf-8"))
    wordList = []
    while node:
        temp = node.feature.split(",")
        if temp[0] == u"名詞":
            # 一般名詞、固有名詞のみ抽出する
            if temp[1] == u"一般" or temp[1] == u"固有名詞":
                wordList.append("%s" % node.surface)
        node = node.next
    return wordList

if __name__ == "__main__":
    # 形態素解析器
    tagger = MeCab.Tagger("-Ochasen")
    
    # 属性抽出用の正規表現
    htmlTagPattern = re.compile(r'<.*?>')
    titlePattern = re.compile(r'TITLE:\s(.*)')
    categoryPattern = re.compile('CATEGORY:\s(.*)')
    datePattern = re.compile(r'DATE:\s(.*)')
    bodyPattern = re.compile(r'BODY:')
    codeStartPattern = re.compile(r'<pre class="syntax-highlight">')
    codeEndPattern = re.compile(r'</pre>')
    
    fin = codecs.open(FILE, "r", "utf-8")
    fout = codecs.open(OUTPUT, "w", "utf-8")
    
    title = category = date = body = ""
    bodyflg = codeflg = False
    for line in fin:
        line = line.rstrip()           # 末尾改行除去
        line = line.replace("\t", "")  # タブ文字除去
        if line == "": continue
        
        # タイトルの抽出
        m = titlePattern.match(line)
        if m:
            title = m.group(1)
        
        # カテゴリの抽出(2つ以上ある場合は1つ目のみ)
        m = categoryPattern.match(line)
        if m and category == "":
            category = m.group(1)
        
        # 日付の抽出
        m = datePattern.match(line)
        if m:
            date = m.group(1)          # 02/21/2002 05:30:47 PM
            date = date.split(" ")[0]  # 02/21/2002
            month, day, year = [int(x) for x in date.split("/")[0:3]]
            date = "%04d-%02d-%02d" % (year, month, day)  # 2002-02-21
        
        # プログラムコード行が見つかったらcodeflgを立てる
        m = codeStartPattern.match(line)
        if m:
            codeflg = True
        
        # プログラムコード終了行が見つかったらcodeflgを下ろす
        m = codeEndPattern.match(line)
        if m:
            codeflg = False
        
        # bodyflg = Trueでコード行以外なら本文行なのでbodyに追加
        if bodyflg and not codeflg and line != "-----":
            # HTMLタグを除去して追加
            body = body + ' ' + htmlTagPattern.sub("", line)
        
        # 本文終了記号が見つかったら1エントリ分のデータを出力
        if bodyflg and line == "-----":
            # タイトルと本文を形態素解析して単語を抽出
            wordList = analysis(tagger, title + body)
            # 単語:カウントの形式で出力するために単語をカウント
            wc = defaultdict(int)
            for w in wordList:
                wc[w] += 1
            # カウントで降順ソート
            lst = wc.items()
            lst.sort(lambda p0,p1: cmp(p1[1],p0[1]))
            # ファイルに出力
            words = []
            for k, v in lst:
                words.append("%s:%d" % (k, v))
            print category, " ".join(words)
            fout.write("%s %s\n" % (category, " ".join(words)))
            bodyflg = False
            title = category = date = body = ""
        
        # 本文が見つかったらフラグを立てる(次の行からbodyに追加)
        m = bodyPattern.match(line)
        if m:
            bodyflg = True
    
    fout.close()
    fin.close()

MeCab Python

形態素解析にはMeCabのインストールが必要です。MeCabは高性能な形態素解析モジュールでPython、Ruby、Perl、Javaなどさまざまな言語から使えます。Mac OS XとLinuxでは簡単にコンパイルしてインストールができるんですが、WindowsではMinGWやVisual Studioのインストール & コードの修正が必要でかなり面倒くさい。そこで、Pythonモジュールはid:fgshunさんがコンパイルしたバイナリを使わせてもらいました。以下、導入方法です。

  1. MeCabの本サイトでダウンロードしたWindows版のmecab-0.98.exeをインストール(辞書はUTF-8形式が無難です)
  2. 形態素解析エンジン MeCab 0.98pre3 野良ビルドからダウンロードしたlibmecab-1.dll、MeCab.py、_MeCab.pydをパッケージフォルダ(Python2.5ならC:\Python25\Lib\site-packages)にコピー。IPA辞書はmecab-0.98.exeでインストールしたので不要。

id:fgshunさんはTaggerにmecabrcファイルを指定してますが、指定しないとデフォルトでC:\Program Files\MeCab\etc\mecabrcを読みにいくようなのでmecab-0.98.exeでIPA辞書をインストールしたほうが簡単だと思います。下のサンプルプログラムが動くか確かめてみます。

#coding:utf-8
import MeCab
s = u"形態素解析にはMeCabを使っているので上のプログラムを動かすには事前にインストールが必要です"
tagger = MeCab.Tagger('-Ochasen')
node = tagger.parseToNode(s.encode('utf-8'))
wordList = []
while node:
    if node.feature.split(",")[0] == u"名詞":
        wordList.append(node.surface)
    node = node.next
for word in wordList:
    print word

実行すると名詞のみが抽出されて出力されます。

形態素
解析
MeCab
上
プログラム
事前
インストール
必要

MeCabはまた使いそうなのであとでまとめとこうかな。形態素解析にはMeCabではなく、Yahoo!形態素解析API(2009/4/15)を用いてもよいと思います。Yahoo!はMeCabより固有表現の抽出精度が高いように思います。

カテゴリ分布

では、話はナイーブベイズに戻ります。まず、カテゴリごとに各記事がどれくらいあるか統計を取ってみました。記事に複数のカテゴリがある場合は第一カテゴリのみ使っています。カテゴリ分布は下のようになりました。20 Newsgroups(2010/6/18)と違ってかなりばらつきがあります。一般的にカテゴリ間のばらつきが大きいと精度が低下することが知られています。ちょっと心配になってきた。

f:id:aidiary:20100703210346p:plain

特徴語抽出

次に各カテゴリで相互情報量(2010/6/19)が高い上位10個の単語を抽出してみます。

[人工知能]
0.0668466792518 知能
0.0605570522974 人工
0.032089567665 学会
0.0254121860825 時代
0.0253081836479 AI
0.0185970342997 技術
0.0182707784424 Norvig
0.0178956959728 機械
0.0160822792793 分野
0.013954708507 康一

[機械学習]
0.0554374306209 ベイズ
0.0487473103335 データ
0.0463269427927 式
0.0431790653499 最小
0.0431790653499 w
0.0430882201539 ベクトル
0.0362336163557 線形
0.0357786049225 パターン
0.0295890551301 誤差
0.0276829622679 t

[自然言語処理]
0.0407276053721 形態素
0.0305270697973 Google
0.0209781544664 マルコフ
0.0209781544664 コンテンツ
0.0198496993147 名詞
0.0198496993147 スパイダー
0.0176073150915 Web
0.0174213092901 連鎖
0.0174213092901 Hacks
0.0174213092901 Eliza

[ロボティクス]
0.0836949238735 ロボット
0.038853580853 AIBO
0.0300575326888 センサー
0.0278687435843 ROBODEX
0.0225409242532 ヒューマノイド
0.0173236338447 ロボカップ
0.0173236338447 インテリジェンス・ダイナミクス
0.0158608128224 日記
0.0149268606495 QRIO
0.0125190099368 産業

[哲学]
0.0696987261079 哲学
0.0466277158515 ゲーデル
0.0433229397643 バッハ
0.0433229397643 エッシャー
0.0402016652066 ホフスタッター
0.0293236094059 ドレイファス
0.0293236094059 ダグラス
0.0278300807178 コンピュータ
0.0230329975586 定理
0.0222695028808 揚

[認知科学]
0.0344178968077 心理
0.0291007471038 動物
0.0215349075037 科学
0.0158318961015 身体
0.0148852974513 自身
0.0140415540977 ヒト
0.0131966937095 相互
0.0129084130782 犬
0.0126732630125 ゲーム
0.0124394410983 報酬

[強化学習]
0.0821109858052 報酬
0.072511052999 エージェント
0.0702965495728 迷路
0.0470118931038 Reinforcement
0.0469046359785 Q
0.0381829229148 振子
0.0380810498439 状態
0.0371525790713 エピソード
0.0325025838501 ブログ
0.0309618292129 テーブル

[複雑系]
0.047560226878 乱数
0.0422215036047 現象
0.0407280367457 ランダム
0.0303254834174 擬似
0.0303254834174 スモール
0.025055107977 ワールド
0.0232314372039 カオス
0.0189085366345 最前線
0.0189085366345 エントロピー
0.0183085362125 確率

[脳科学]
0.114387617974 脳
0.0314007895288 コンピューター
0.0201049064124 患者
0.0196755440911 大脳
0.0161414204785 茂木
0.0143327777787 科学
0.0133952718291 健一郎
0.0124255845359 つながり
0.0122488069338 海馬
0.0108136899383 筑摩書房

[生物学]
0.0311204925831 バイオインフォマティクス
0.0304531477327 細胞
0.025867712687 生物
0.0160213110636 動物
0.0133887567948 生命
0.0130598253523 見方
0.0130598253523 個体
0.0104193172884 遺伝子
0.00996531603537 生き物
0.00927583404748 ヒト

おおおおお。地味だけどけっこうすごいぞ。

交差検定(Cross Validation)

最後にナイーブベイズの分類精度を評価してみます。20 Newsgroupsは文書数が20000件と多かったので訓練データとテストデータを完全に分けてから分類精度を評価しました。しかし、ブログ記事は全部で760件しかないので独立したテストデータを用意するのは少しもったいないです。そこで、交差検定(N-fold Cross Validation)という評価法を使ってみます。N-foldの代わりにK-foldを使うことが多いようですが、すでにボキャブラリの数で文字Kを使ってしまったので代わりにNを使いました。N-fold Cross Validationは全データを均等にN分割し、その中から1個をテストデータとし、残りのN-1個を訓練データとする評価法です。テストデータの選び方はN通りあるためN通りの訓練データとテストデータのそれぞれで分類精度を求め、最後に平均を取って最終精度とすることが多いです。ちなみに分割数Nをデータ数と同じにした場合はLeave-one-outと呼びます。

では、Cross Validationのコードです。Recipe 521906: K fold cross validation partition (Python)のコードを参考にしました。剰余を使ってデータを分けるのはけっこううまいやり方だなぁ。下のコードを動かすには相互情報量を用いた特徴選択(2010/6/19)で作ったナイーブベイズ(naivebayes_fs.py)と特徴選択(feature_selection.py)が必要です。ボキャブラリの数は適当に1000としました(相互情報量が高い順に1000語を使います)。

#coding:utf-8
import codecs
import sys
from naivebayes_fs import NaiveBayes  # 特徴選択付きのナイーブベイズを使う
from pylab import *

# 特徴抽出に使う単語数
K = 1000

def crossValidation(data, N=5, randomize=False):
    """N-fold Cross Validationで分類精度を評価"""
    # データをシャッフル
    if randomize:
        from random import shuffle
        shuffle(data)
    
    # N-fold Cross Validationで分類精度を評価
    accuracyList = []
    for n in range(N):  # 各分割について
        # 訓練データとテストデータにわける
        trainData = [d for i, d in enumerate(data) if i % N != n]
        testData = [d for i, d in enumerate(data) if i % N == n]
        # ナイーブベイズ分類器を学習
        nb = NaiveBayes(K)
        nb.train(trainData)
        print nb
        # テストデータの分類精度を計算
        hit = 0
        numTest = 0
        for d in testData:
            correct = d[0]
            words = d[1:]
            predict = nb.classify(words)
            if correct == predict:
                hit += 1
            numTest += 1
        accuracy = float(hit) / float(numTest)
        accuracyList.append(accuracy)
    # N回の平均精度を求める
    print accuracyList
    average = sum(accuracyList) / float(N)
    return average

if __name__ == "__main__":
    # ブログデータをロード
    data = []
    fp = codecs.open("blog.txt", "r", "utf-8")
    for line in fp:
        line = line.rstrip()
        temp = line.split()
        data.append(temp)
    fp.close()
    # N-fold Cross Validationで分類精度を評価
    average = crossValidation(data, N=10, randomize=True)
    print "accuracy:", average

実行すると

accuracy: 0.651315789474

20 Newsgroupsは80%だったので少し落ちますけど思っていたより高いなぁ。

まとめ

今回は、日本語のブログ記事のデータをナイーブベイズで分類してみました。実データらしくデータ数の偏りが大きかったですがそこそこな分類精度が出ました。今回はボキャブラリ数とか抽出する形態素の品詞選択とかかなり適当なので本来ならもっとちゃんとした検証が必要です。とは言いながら、次回はPRMLで学んだ(2010/5/3)サポートベクトルマシン(SVM)を使ってブログ記事を分類してみます。SVMはテキスト分類で高精度を叩き出すことで有名な手法です。どれくらい精度が上がるか楽しみだなぁ。