nekoneko_genで20newsgroupを試してみた

文書分類では、20newsgroupsというデータセットがよく使われるようなので、英語テキストの対応をしながら試してみました。

http://people.csail.mit.edu/jrennie/20Newsgroups/

これは20種類のニュースグループに投稿された約2万件のドキュメントを含むデータセットです(学習用が1.1万件、確認用が7.5千件だった)。
ニュースグループというのは、メーリングリストで2ちゃんねるをやっている感じのものだと思います。
20種類の板に投稿されたレスをどの板の投稿か判定するマシンを学習する感じ。


(注意: ここに書かれている作業用のコードはRuby1.9系でしか動きません)

最新のnekoneko_genにアップデート

まず

gem update nekoneko_gen

とアップデートします。
これを書いている時点の最新は0.4.0です。
入っていない場合は、

gem install nekoneko_gen

でインストールされます。

データの準備

サイトを見ると何種類かありますけど、20news-bydate.tar.gz を使います。

% wget http://people.csail.mit.edu/jrennie/20Newsgroups/20news-bydate.tar.gz
% tar -xzvf 20news-bydate.tar.gz
% ls 
20news-bydate-test  20news-bydate-train

train用とtest用に分かれているらしいので、trainで学習して、testで確認します。
構造を見よう。

% ls 20news-bydate-train
alt.atheism    comp.os.ms-windows.misc   comp.sys.mac.hardware  misc.forsale  rec.motorcycles     rec.sport.hockey  sci.electronics  sci.space               talk.politics.guns     talk.politics.misc
comp.graphics  comp.sys.ibm.pc.hardware  comp.windows.x         rec.autos     rec.sport.baseball  sci.crypt         sci.med          soc.religion.christian  talk.politics.mideast  talk.religion.misc
% ls  20news-bydate-train/comp.os.ms-windows.misc
10000  9141  9159  9450  9468  9486  9506

20news-bydate-trainと20news-bydate-testの下に各カテゴリのディレクトリがあって、各カテゴリのディレクトリにドキュメントがファイルに分かれて入っているようです。

nekoneko_genは、1ファイル1カテゴリ1行1データの入力フォーマットなので、まずこんなスクリプトで変換します。

# coding: utf-8
# 20news-conv.rb
require 'fileutils'
require 'kconv'

src = ARGV.shift
dest = ARGV.shift
unless (src && dest)
  warn "20news-conv.rb srcdir destdir\n"
  exit(-1)
end

FileUtils.mkdir_p(dest)
data = Hash.new
# 元データの各ファイルについて
Dir.glob("#{src}/*/*").each do |file|
  if (File.file?(file))
    # root/category/nに分解
    root, category, n = file.split('/')[-3 .. -1]
    if (root && category && n)
      data[category] ||= []
      # ファイルの内容を改行をスペースに置き換えて(1行にして)カテゴリのデータに追加
      data[category] << NKF::nkf("-w", File.read(file)).gsub(/[\r\n]+/, ' ')
    end
  end
end

# 出力側で
data.each do |k,v|
  # カテゴリ名.txtのファイルにデータを行単位で吐く
  path = File.join(dest, "#{k}.txt")
  File.open(path, "w") do |f|
    f.write v.join("\n")
  end
end

train、testというディレクトリに変換。

% ruby 20news-conv.rb 20news-bydate-train train
% ruby 20news-conv.rb 20news-bydate-test test
%
% ls test
alt.atheism.txt              comp.sys.ibm.pc.hardware.txt  misc.forsale.txt     rec.sport.baseball.txt  sci.electronics.txt  soc.religion.christian.txt  talk.politics.misc.txt
comp.graphics.txt            comp.sys.mac.hardware.txt     rec.autos.txt        rec.sport.hockey.txt    sci.med.txt          talk.politics.guns.txt      talk.religion.misc.txt
comp.os.ms-windows.misc.txt  comp.windows.x.txt            rec.motorcycles.txt  sci.crypt.txt           sci.space.txt        talk.politics.mideast.txt
% head alt.atheism.txt

できてる。

学習

1コマンドです。ここまでの作業のことは忘れましょう。分類器の名前はnews20にしました。
trainの下を全部指定します。

% nekoneko_gen -n news20 train/*

ちょっと時間かかります。

% nekoneko_gen -n news20 train/*
loading train/alt.atheism.txt... 11.2039s
loading train/comp.graphics.txt... 10.0659s
loading train/comp.os.ms-windows.misc.txt... 24.7611s
loading train/comp.sys.ibm.pc.hardware.txt... 9.1767s
loading train/comp.sys.mac.hardware.txt... 8.3413s
loading train/comp.windows.x.txt... 13.9806s
loading train/misc.forsale.txt... 6.8255s
loading train/rec.autos.txt... 9.9041s
loading train/rec.motorcycles.txt... 9.4798s
loading train/rec.sport.baseball.txt... 9.9481s
loading train/rec.sport.hockey.txt... 14.2056s
loading train/sci.crypt.txt... 19.5707s
loading train/sci.electronics.txt... 9.6204s
loading train/sci.med.txt... 13.6632s
loading train/sci.space.txt... 14.4867s
loading train/soc.religion.christian.txt... 16.4918s
loading train/talk.politics.guns.txt... 16.2433s
loading train/talk.politics.mideast.txt... 21.8133s
loading train/talk.politics.misc.txt... 16.2976s
loading train/talk.religion.misc.txt... 10.4111s
step   0... 0.953548, 58.1906s
step   1... 0.970537, 47.1082s
step   2... 0.980550, 41.9248s
step   3... 0.985889, 37.6781s
step   4... 0.989483, 35.0408s
step   5... 0.991824, 33.8357s
step   6... 0.993727, 30.1385s
step   7... 0.995139, 29.5926s
step   8... 0.996107, 29.3976s
step   9... 0.997182, 27.9107s
step  10... 0.997546, 27.1800s
step  11... 0.998004, 26.4783s
step  12... 0.998581, 26.7023s
step  13... 0.998985, 25.9511s
step  14... 0.999145, 24.7697s
step  15... 0.999324, 24.7991s
step  16... 0.999430, 24.8879s
step  17... 0.999569, 24.7246s
step  18... 0.999622, 25.2175s
step  19... 0.999615, 23.5225s
ALT_ATHEISM : 153334 features
COMP_GRAPHICS : 153334 features
COMP_OS_MS_WINDOWS_MISC : 153334 features
COMP_SYS_IBM_PC_HARDWARE : 153334 features
COMP_SYS_MAC_HARDWARE : 153334 features
COMP_WINDOWS_X : 153334 features
MISC_FORSALE : 153334 features
REC_AUTOS : 153334 features
REC_MOTORCYCLES : 153334 features
REC_SPORT_BASEBALL : 153334 features
REC_SPORT_HOCKEY : 153334 features
SCI_CRYPT : 153334 features
SCI_ELECTRONICS : 153334 features
SCI_MED : 153334 features
SCI_SPACE : 153334 features
SOC_RELIGION_CHRISTIAN : 153334 features
TALK_POLITICS_GUNS : 153334 features
TALK_POLITICS_MIDEAST : 153334 features
TALK_POLITICS_MISC : 153334 features
TALK_RELIGION_MISC : 153334 features
done nyan!

終わったらnews20.rbというRubyのライブラリが生成されています。

% ls -la news20.rb
-rw-r--r-- 1 ore users 66599221 2012-06-02 17:10 news20.rb

60MB以上あります。デカい。

確認

20カテゴリもあって前回のスクリプトで1カテゴリずつ見るのはきついので、一気に確認するスクリプトを書きました。

# coding: utf-8
# test.rb

# 分類器を読み込む
require './news20'

# ファイル名をnekoneko_genが返すラベル名に変換する関数
def label_name(file)
  File.basename(file, ".txt").gsub(/[\.\-]/, "_").upcase
end

count = 0
correct = 0
# 指定された各ファイルについて
ARGV.each do |file|
  # ファイル名からラベル名を得る
  name = label_name(file)
  
  # ラベル名から正解ラベル(定数)に変換
  # (News20::LABELSにラベル番号順のラベル名があるので添え字位置を探す)
  correct_label = News20::LABELS.each_with_index.select{|v,i| v == name}.flatten.pop
  
  file_count = 0
  file_correct = 0
  # ファイルの各行データについて
  File.read(file).lines do |l|
    # 予測
    label = News20.predict(l)
    # ラベルが一致していたら
    if (label == correct_label)
      # 正解!!
      file_correct += 1
    end
    # データ数
    file_count += 1
  end
  correct += file_correct
  count += file_count
  # ファイルの内での正解率を表示  
  printf("%26s: %f\n", name, file_correct.to_f / file_count.to_f)
end
# 全体の正解率を表示  
printf("\nAccuracy: %f\n", correct.to_f / count.to_f)

testの下を全部指定します。

% ruby test.rb test/*
               ALT_ATHEISM: 0.789969
             COMP_GRAPHICS: 0.825193
   COMP_OS_MS_WINDOWS_MISC: 0.753807
  COMP_SYS_IBM_PC_HARDWARE: 0.778061
     COMP_SYS_MAC_HARDWARE: 0.867532
            COMP_WINDOWS_X: 0.815190
              MISC_FORSALE: 0.902564
                 REC_AUTOS: 0.916667
           REC_MOTORCYCLES: 0.969849
        REC_SPORT_BASEBALL: 0.957179
          REC_SPORT_HOCKEY: 0.984962
                 SCI_CRYPT: 0.952020
           SCI_ELECTRONICS: 0.778626
                   SCI_MED: 0.881313
                 SCI_SPACE: 0.936548
    SOC_RELIGION_CHRISTIAN: 0.937186
        TALK_POLITICS_GUNS: 0.934066
     TALK_POLITICS_MIDEAST: 0.914894
        TALK_POLITICS_MISC: 0.616129
        TALK_RELIGION_MISC: 0.677291

Accuracy: 0.866171

86.6%でした。