こまめに日記を書きたい

昔は、開発日記とか、日々のムカついた事とか、自分の精神状態の報告とか、まあいろいろとちょっとしたことを書いていたのに、最近はまとめしか書かなくなってきてて、でも、やっぱり書いたほうがいいと思うので、もう少し気軽にやっていることの途中経過とかを書こうと思う。
前にも同じ事を言ってて結局書かなくなったので、繰り返しかもしれないけど。

まとめると、やってたことをまとめようとしているけどつらいので適当に書いていきたいということです。

Ubuntu 12.10でL4D2をする (Wine)

SteamのLinux Betaが出たらしいです。僕は2週間くらい前にベータのアンケートみたいなやつに答えたのですが、特に何も来てなくて試せていません。試せていませんが、がんばってほしいですね。試せていませんが。
ただ僕は、Ubuntu 12.04の頃からWine経由でL4D2をやっていて、Wineってなんかめんどうなイメージがあったのですが(APIがないと言われるたびに実装していくみたいな…)、L4D2は素直に動いたので、超簡単な方法を書いておきます。

グラボなんかの環境にも依存してそうなので環境によっては動かないかもしれません。うちのグラボはGTX 260でドライバはnvidia-currentを使っています。デスクトップはgnome-classicです。

Wineのインストール
sudo apt-get install wine
Steamのインストール
winetricks steam

Steamのインストーラーが起動するのでインストールします。

起動後にSteamのアカウント情報を入力すると、メールで認証コードを送ったので入力しろと言われるので、メールで受け取った認証コードを入れるとログインできます。

L4D2のインストール

SteamのライブラリからL4D2を選んでインストールします。
http://943caf378f4d1129752df2954803f96c.gazou.me/large.png
持っていない人はストアで買いましょう。(買ったものの動かないことがあるかもしれません。動かなかったらWindowsでやりましょう)

これで普通に起動するので、とりあえずシングルプレイヤーモードで入ってビデオやコントローラーの設定をするといいと思います。CAPSLOCKをCTRLに変えててもWineだと変わってなかったりします。
http://81f4744f23b5f64cd49d49d4eeb63ac1.gazou.me/large.png

Steam Cloudで設定が同期されるようになっていると、他のパソコンの設定を変えてしまうので注意してください。多分最初に起動したときに同期するか聞かれます。

便利化

ニワカですが便利化します。

起動時のムービーを流さないようにする

Steamのライブラリ→L4D2→プロパティ→起動設定で、-novid を入れておくと起動時のムービー流れなくなります。

steam://のURLを開く

xdg-openをいじればsteam://を開けそうですが、ウェブ側から開けるとちょっと怖い感じするので僕はコマンドにしています。

$HOME/bin/steam-open に

#!/bin/sh
env WINEPREFIX="$HOME/.local/share/wineprefixes/steam" wine "C:\\windows\\command\\start.exe" "$@"

のようなスクリプトを置いて$HOME/binにPATHを通しておくと、たとえば、nico-opなら

$ steam-open steam://connect/l4d2.forjp.net:27032 

で接続できます。

よくいるサーバーがある場合はサーバーごとにコマンドを作ったり、ショートカットをデスクトップ等に置くと便利かもしれません。

(winetricksから入れると$HOME/.local/share/wineprefixes/steamに入るっぽいです。場所が違う場合は、ウィンドウマネージャーのメニューにWine/Programs/Steamが入っていると思うのでプロパティを見ましょう。)

不具合

重いローカルサーバーや人数が多いサーバーに入って負荷がかかった時に、

  • 入った瞬間にサウンドループが起こる(大体は5秒くらいで直るけどそのまま固まることもある)
  • 一部の音(特殊の声だけとか自分の武器の音だけとか)が聞こえなくなる
  • wなどの移動キー入力が入ったままになって解除できなくなる

などの不具合が出ることがあります。Steamを再起動しないと直らないので、操作できる場合はいったん落としましょう。全画面で固まっている場合は、ALT+TABで抜けて、xkillコマンドでL4D2を落とすか、マジで固まっててどうもなくなったらCTRL+ALT+DELなどでウィンドウマネージャーからログアウトして、入りなおしてからkillコマンドで止めます。

他のゲーム

HL2は起動したけど、移動キー入力が入ったままになって解除できなくなる不具合が頻発して無理だった。
TF2は起動時のムービーで固まる。CoD2は起動できなかった。Valve以外のゲームは無理っぽい感じする。

L4D2だけは稀に不具合に出るだけで結構ちゃんと動いています。

なぜLinuxなのか!!

最近パソコンが1台しかなくて、それで時間がかかる計算の処理をLinux側で動かしていることがあるので(Windows7デュアルブートになってる)、その合間にゲームをするためにLinuxで動かしています。

この場合、更にcgroupの仕組みを使うと、計算用プロセスとゲーム用プロセスで使うCPUを分けれるので、ゲームが重くならない程度のリソースを割り当ててできるだけ計算に回すみたいなことができて便利です。

はてなブログに移行した

ダイアリーから記事をインポートしてリダイレクトの設定もできるようになっていたので移行しました。

6月くらいからできていたらしい。 はてなブログがリリースされた時に出来なくてアホカと思ったまま興味を失っていたので気づかなかった。ベータ版だったから突然大移動されても困るみたいなのがあったのだろうけど。

今は、記事自体とコメントをコピーして元のURLやRSSはリダイレクトできて、記事についていたはてなスターはてブのURLも置換?されるようです。

ダイアリーの方も広告が出るようになったし、もうあっちにデータを置いていてうれしいことなんてないと思うので、さっさとはてなブログに移行したほうがいいと思いました。

参照

はてなダイアリーからはてなブログへのインポート機能を追加しました

表紙を写すと本のレビューが読めたりするHTML5アプリを作ってみた

最近のブラウザはgetUserMediaという機能でJavaScriptからカメラを操作してリアルタイムに動画を読んだりできるようなので、画像認識のデモ的なものを作ってみた。

表紙検索

PCのGoogle ChromeOperaAndroidOpera Mobileの場合

動画が読めるといっても実際のところ一部のブラウザでしか使えなかったので段階的に対応してみた。
まず、PCのGoogle ChromeOperaAndroidOpera Mobileの最新バージョンが全ての機能が使えるブラウザです。

これらのブラウザで hyoushi.me というドメインを開くと、サイトのトップに「カメラ起動」というでかいボタンが表示されています。

IS05 + Opera Mobile


(僕はウェブカメラを持っていないのでこれはIS05 + Opera Mobileのキャプチャです)

カメラ起動ボタンをクリックすると、こんな感じでページ内にカメラからの映像が表示されます。

このまま適当に床に落ちている本を写して映像部分をクリックすると、決定かと聞いてくるので、

OKを押すと映像がブラウザ上でリサイズされた後サーバーに送信されて、

検索結果が表示されます。

サーバーは さくら VPS 2GB 1台で動かしていて、今僕が一人で使っている分には1秒ちょっとで結果が帰ってきていますが、他の人が使っていると激重になるかもしれません。

検索結果はとりあえず2件返しているので、正解があれば正解の画像をクリックすると、本の詳細やレビューなどが表示されます。

ここでAmazonへというボタンをクリックして購入すると僕にアフィ収入が入るというビジネスモデルを採用しています。


僕のIS05だと動画の解像度が240x320しかなくて、かなり画質が悪いし、動画なのでピントを合わせたりが難しいのですが、結構適当に写しても検索されます。

2冊同時でもいける。タイトルをアップで写すと似たようなフォントで同じ文字列を含むタイトルが検索されたりもします。
ぶれてたり、反射していたり、遠すぎて本が小さかったり、斜めになってパースが効いていたりしなければ大体目的の本が出てくるのではないかと思います。
全体を写そうとするよりは、タイトルやイラストなど特徴的な部分をアップでくっきり写したほうが精度がよいです。

家にあった本を大体試したけど、Amazon.co.jp: 時間衝突 (創元推理文庫): バリントン・J・ベイリー, 大森 望: 本という本がどうやっても出なかった以外は、出てたので、大体出るのではないかと思います。(カメラにもよるかもしれない)

あと検索結果として表示される本の詳細や表紙画像、レビューなど全ての情報はAmazonのItemLookupで検索毎に最新の情報を取得しているので、検索した時点でAmazonに無い本は出てきません。
またAmazonのAPIは1時間に2000回しか叩けないので、このサイトも(一人あたりではなく全体で)1時間に2000回しか検索できません。

Windows7 + Opera

ウェブカメラは持っていないのですが、ニコニコミュニティという仮想カメラソフトを使うと、デスクトップや指定した写真をカメラの入力にできるらしいので一応動くか試してみた。

普通に動いた。動画サイズ(解像度)はJavaScriptから指定できないので、カメラによって縦横比が変わったりします。一応はみ出ないように最大幅を超えている場合はフレームをリサイズして表示しています。

Windows 7 + Google Chrome

Google ChromeWindows XPでは動いたけど、Windows7では動きませんでした(いまWindows7なのでキャプチャは無い)。
これはたぶん、
Issue 136238 - chromium - getUserMedia stopped working on Windows - An open-source project to help move the web forward. - Google Project Hosting
のバグで、機種によってはカメラの初期化に失敗するようなので、パソコンの場合は実質対応しているのはOperaだけかもしれません……

getUserMediaに対応していない場合

JavaScriptからカメラが操作できない場合は、普通にファイル送信しかありません。画像ファイルを選択するとその画像で検索できます。便利。


(これはFirefox Mobileのキャプチャです)

ただカメラが内蔵されている機器を使っている場合は、ファイル選択からカメラが選択できる場合があって

この場合は、普通にカメラアプリが起動して撮影すると検索されます。こっちのほうがフォーカスが効くし、解像度が高いのでいい気がしますね……
ただ、この場合だとたまに撮影後にブラウザがリロードされて検索できないことがあって、ブラウザのバグなのか僕が悪いのか分かっていないです。あとなにか重い感じするし、シャッター音がします。

僕のAndroid 2.3 標準ブラウザでは、カメラ選択は出なくて無理でした。

追記

ファイルが選択された場合(ファイル選択からカメラを起動した場合)にFile APIで読み込んでリサイズしてアップロードしていたのですが、iPhone5のSafariで画像がバグるらしいのでとりあえず無効にしました(そのままファイルアップロードする)。これだとiPhone5のSafariでも使えているようです。

まとめ

JavaScriptで動画は扱えるけどブラウザを選んだり、動画なのでフォーカスが効かなかったり、解像度が低かったり、散々な感じでしたが、全体的には未来っぽい感じにできたと思います。携帯端末とか得に意識しなくてもブラウザが対応していれば勝手に動いてくれるのもいいです。Javaとか環境を作るだけでだるいですし。

本の検索に関しては、僕の感覚ではこの規模のデータを『さくら VPS 2GB 1台』でこの精度この速度で検索できるなら、現時点で実用レベルに達していると思うので、"きれいなデータ"さえあればいろいろ応用ができるのではと思います。
僕はデータを提供することはできないですが(そもそも画像データ自体は保存していない)、このサイトで使っている画像検索エンジンのライブラリを公開しているので、興味のある方は使ってみてください。

nagadomi/otama · GitHub
このotamaというライブラリの bovw512k_iv_kc というドライバを使っています。

このライブラリは2年くらい前に前職の関係で開発したもので、その後も地道にいじりながらなんか使い道ないかなと思っていたのですが、特に使い道が見つからないまま先月退職したのと、個人で作っていてクレジットは僕が持っていたので、githubにでも置いとくことになったものです。
2年も前から作っているならすごく完成度が高そうだけど、ここ1ヵ月くらい無職の力を使ってハチャメチャに書き直したので、まだかなりアルファっぽい感じです。ベースが2年前なのでいろいろ古いし。
詳しくはREADME.mdなどを見てください。

今年中くらいはフラフラしてようと思うので、その間気が向いたらいじったりすると思いますのでよろしくお願いします。

組み込み型お手軽全文検索エンジンのRubyライブラリを作って2ちゃんねるQ&A検索の全文検索エンジンを置き換えた

2ちゃんねるQ&A検索(非公式)全文検索は pg_trgm を使っていたのですが、だんだんとデータが増えてくるとPostgreSQLの共有メモリを増やさないと検索がタイムアウトするようになってきて、最近ではVPSのメモリのほとんどをPostgreSQLに割り当ててもクソ重くてやばかったので、昨日、意を決して組み込み型お手軽全文検索エンジンをテキトウに作って置き換えたら爆軽爆速になった。

qarc.info は意外に月100万訪問者以上あるんですけど、月800円のVPS1台にウェブサーバーからデータベースサーバーから全部突っ込んだ運用をしていて、これでどこまでいけるかというのが個人的な趣味みたいな感じなので、今回もできるだけ軽い実装かつ導入がめんどくさくないものを作ってみました。半日くらいで適当に作って、組み込みながらちょこちょこ直したもので、あまりいいものではないんですけど、その紹介をします。

FtsLite

FtsLiteは組み込み型お手軽全文検索エンジンRubyライブラリです。
基本的にはSQLite3 FTS4のラッパーで、日本語のbigramやtrigram、wakachi_bigramなどのトークナイザーをRubyのレイヤで実装したものです。

qarc.info で使われています。

Ruby 1.9.2 以降と、FTS4 に対応してる SQLite3 が必要で、SQLite3 はできれば 3.7.7 以降がよいです(FTS4の仮想テーブルに対するINSERT OR REPLACEが実装されているのでsetのパフォーマンスがよい)。

インストール
gem install fts_lite
Usage
# -*- coding: utf-8 -*-
require 'fts_lite'

FtsLite::Index.open("./index.sqlite3") do |db|
  # set(docid, text, sort_value = nil)
  db.set(1, "なぜナポリタンは赤いのだろうか?")
  db.set(2, "昼飯のスパゲティナポリタンを眺めながら、積年の疑問を考えていた。")
  # docid_array = search(query, options = {})
  docids = db.search("ナポリタン")
  puts docids.join(",")
  docids = db.search("赤い ナポリタン")
  puts docids.join(",")

  # update_sort_value(docid, sort_value)
  db.update_sort_value(1, 2)
  db.update_sort_value(2, 1)

  docids = db.search("ナポリタン", :order => :asc, :limit => 1)
  puts docids.join(",")
end
1,2
1
2

set(docid, text, sort_value)でデータを登録します。
docidはレコードを表すIDで search ではこのIDの配列を検索結果として返します。
textは全文検索インデックスのためのテキストデータです。取り出すことはできないので、元のデータは別のデータベースにあることを想定しています。
sort_valueはソート用の値で、searchの時にこの値でソートしたり、ソートしたうえで上位N件を取り出したりできます。

search(query, options) で検索できて、queryは空白区切りでAND検索です。このへんの仕様は用途によっていろいろだと思うけど、僕はテキトウにANDだけできればいいやと思っているので、いじりたい人は lib/tokenizer.rb で定義してある query というメソッドをいじってください。
optionsは :order に :asc か :desc を指定すると sort_value で昇順ソートまたは降順ソートします。 :order が指定されない場合は docid の昇順になります。:limit => N を指定すると検索結果の上位N件だけを返します。

Railsで使う

まず config/application.rb あたりで

QUESTION_FTS = FtsLite::Index.open(File.join(Rails.root.to_s, "fts", "index.sqlite3"), :table_name => "questions")
THREAD_FTS = FtsLite::Index.open(File.join(Rails.root.to_s, "fts", "index.sqlite3"), :table_name => "threads")
# ...

とコネクションを作ってグローバルにアクセスできるようにしておきます。
モデルでしか使わない場合は、モデルの中で定義したほうがいいかもしれません。
:table_name を指定するとひとつのファイルに複数の全文検索インデックスが持てます。

あとは、たとえば、Question という ActiveRecord のモデルがあって、全文検索用のテキストデータ(内容やタイトルなんかを適当に結合した文字列)を作成する make_ft というメソッドとソート用の表示数 view_count というカラムがあるとすると

class Question < ActiveRecord::Base
   LIMIT = 1000
   after_save :set_ft

   def set_ft
     QUESTION_FTS.set(id, make_ft, view_count)
   end
   def self.search(query)
     find(:all,
          :conditions => ["id in (?)", QUESTION_FTS.search(query, :order => :desc, :limit => LIMIT)],
          :order => "view_count DESC")
   end
   # def make_ft ...
end

まず after_save でレコードの更新時に全文検索インデックス側も更新するようにしておきます。
Question.search というメソットでは、全文検索を行って、その結果(IDの配列)を含むレコードをさらにDBに問い合わせることで検索結果となるレコードを返しています。

この実装だとレプリケーションしている場合に、別のサーバーでレコードが更新されるとトリガーが効かなくてローカルの全文検索インデックスが更新されないことに注意してください。

トークナイザー

デフォルトでは:bigramになっています。openのオプションで、:tokenizer => :wakachi_bigram などと与えると変えれるのですが、日本語の場合、:bigram以外は微妙なので、変えなくていいと思います。英語の場合は :trigram を指定するとよいと思います。
変えれるようにしてみたものの変えなくていい感じで微妙機能です。

感想

SQLite3 の FTS は昔は別モジュールになっていろいろめんどくさかったのですが、最近は組み込まれてるようで、Ubuntuにデフォルトで入っているようなlibsqlite3でも有効になっていたので、テキトウに使える軽量全文検索エンジンとして便利だと思います。

BimyouSegmenter: Rubyだけで書かれた微妙なサイズの分かち書きソフトウェア

BimyouSegmenterはRubyだけで書かれた微妙なサイズの日本語分かち書きソフトウェアです。
約150Kバイトの微妙なサイズのソースコードで、青空文庫にある太宰治宮沢賢治夏目漱石夢野久作作品であれば、MeCab+ipadicによるの分かち書きの結果と94%くらいは同じになります。

というTinySegmenterのパロディです。
TinySegmenterは新聞記事で学習されているのと、空白文字が単語にくっついたりして扱いが難しかったので、TinySegmenterのようなひとつのソースコードに全部つっこんだ感じの分かち書きのライブラリを自分の用意したデータで学習して好きに調節できたらいいなと思ったので作ってみました。
nekoneko_genでもこれを使っています。

TinySegmenterとは特徴ベクトルも学習アルゴリズムも学習データも違いますが、基本的な考え方はTinySegmenterのソースコードから得られる情報を参考にしています。
(ある文字の間で切るか切らないかを、そこを中心とした前後N文字の文字と文字種のN-gram、それまでの分割状態を特徴ベクトルにして線形分類器で判定する)

BimyouSegmenterは、(青空文庫の)太宰治宮沢賢治夏目漱石夢野久作 作品、高田力 ベーシック英語、(Project Gutenbergの) Alice's Adventures in Wonderland, by Lewis Carroll に対するMeCab+ipadicによる分かち書き結果(約800万件)を LIBLINEAR の L1-regularized logistic regression で学習して、学習されたModelファイルからRubyのコードを生成しています。

インストール

gem install bimyou_segmenter

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

コマンドラインツールとライブラリがあります。

コマンドラインツール

% bimyou_segmenter
今年もよろしくお願いします。Happy New Year 2013!
今年
も
よろしく
お願い
し
ます
。
Happy
New
Year
2013
!
EOS
いろいろあったけど、ぼくはげんきです
いろいろ
あっ
た
けど
、
ぼく
は
げんき
です
EOS

オプションは、
-dでデリミタを指定するとその文字列で単語間を区切ります。デフォルトは\nです。
-eでEOSの文字列を指定できます。デフォルトはEOSです。
-sを付けると空白文字や改行コードも単語として返します。-sを付け場合、分割後の単語を繋げると元の文字列と一致します。

% bimyou_segmenter -d "|"
今年もよろしくお願いします。Happy New Year 2013!
今年||よろしく|お願い||ます||Happy|New|Year|2013|!|EOS

% bimyou_segmenter -s -d "<>" -e ""
今年もよろしくお願いします。Happy New Year 2013!
今年<><>よろしく<>お願い<><>ます<><>Happy<> <>New<> <>Year<> <>2013<>!

% bimyou_segmenter --help

ライブラリ

# coding: utf-8
require 'bimyou_segmenter'

puts BimyouSegmenter.segment("今年もよろしくお願いします。Happy New Year 2013!").join("|")
puts BimyouSegmenter.segment("今年もよろしくお願いします。Happy New Year 2013!",
                             :white_space => true).join("|")
今年|も|よろしく|お願い|し|ます|。|Happy|New|Year|2013|!
今年|も|よろしく|お願い|し|ます|。|Happy| |New| |Year| |2013|!

:white_spaceをtrueにすると(default: false)、空白文字や改行も単語して返します。このオプションを指定した場合は、結果の文字列配列を順に結合すると元の文字列と一致します。
また:symbol => falseにすると(default: true)、記号のみからなる単語(トークン)を返しません。

Ruby 1.8.7の場合は、require 'rubygems'と$KCODEをuにすると使えます。

TinySegmenterとの違い(勘)

TinySegmenterは連続する漢字を繋げやすいですが、BimyouSegmenterは連続する漢字を2個ずつで分けることが多いです。たぶん学習データの違いによるものだと思います。
TinySegmenterは空白文字(スペースタブ改行)も返しますが、BimyouSegmenterはデフォルトでは返しません。またBimyouSegmenterは空白文字をルールベースで分割しているので、空白文字と空白文字以外がひとつの単語として結合されることはありません。

MeCab+ipadicとの違い

MeCab+ipadicは半角スペースやタブ、改行コードを返しませんが全角スペースは返します。BimyouSegmenterは:white_spaceオプションを指定した場合を除いて全角スペースも返しません。
(char.defによりそうですが)MeCabは全角数字を文字ごとに分割しますが、BimyouSegmenterは結合しやすいです。
BimyouSegmenterは辞書を持っていないので、3文字以上連続する漢字の多い文章やひらがなの多い文章は超苦手です(大体うまくいかない)。MeCabが使えるならMeCabを使いましょう。

BimyouSegmenter、MeCab、TinySegmenter(Ruby Version)の結果を目視で比較するコード

TinySegmenterはTinySegmenterをRubyに移植 - llameradaの日記Ruby移植版を1.9.2対応してカレントディレクトリに置いて使っています。

# coding:utf-8
if (RUBY_VERSION <= "1.9.0")
  $KCODE='u'
  require 'rubygems'
end
require './tiny_segmenter'
require 'bimyou_segmenter'
require 'MeCab'
require 'kconv'

@@mecab = MeCab::Tagger.new
def mecab_segment(s)
  node = @@mecab.parseToNode(s)
  wakachi = []
  while node
    wakachi << node.surface.toutf8
    node = node.next
  end
  wakachi[1 ... -1]
end

data = <<DATA
昼飯のスパゲティナポリタンを眺めながら、積年の疑問を考えていた。
それは「なぜナポリタンは赤いのだろうか」という問いである。
簡単に見えて、奥の深い問題だ。
「赤いから赤いのだ」などとトートロジーを並べて悦に入る浅薄な人間もいるが、
それは思考停止に他ならず、知性の敗北以外なにものでもない。
「赤方偏移」という現象がある。
宇宙空間において、地球から高速に遠ざかる天体ほどドップラー効果により、
そのスペクトル線が赤色の方に遷移するという現象である。
つまり、本来のナポリタンが何色であろうとも、ナポリタンが我々から
高速で遠ざかっているとすれば、毒々しく赤く見えるはずなのだ。
目の前のナポリタンは高速で動いているか否か?
それはナポリタンの反対側に回ってみることでわかる。
運動の逆方向から観察することで、スペクトルは青方遷移し、
青く見えるはずなのだ。
逆に回ってみたところ、ナポリタンは赤かった。
よってこのナポリタンは高速移動をしていないと言える。
DATA
data = data.gsub(/\s/, '')

puts "\n---- BimyouSegmenter"
puts BimyouSegmenter.segment(data).join("|")
puts "\n---- MeCab"
puts mecab_segment(data).join("|")
puts "\n---- TinySegmenter(Ruby Version)"
puts TinySegmenter.segment(data).join("|")
puts "\n"

t = Time.now
1000.times do
   BimyouSegmenter.segment(data)
end
puts "BimyouSegmenter              1000 loop #{Time.now - t} sec"

t = Time.now
1000.times do
  mecab_segment(data)
end
puts "MeCab                        1000 loop #{Time.now - t} sec"

t = Time.now
1000.times do
  TinySegmenter.segment(data)
end
puts "TinySegmenter(Ruby Version)  1000 loop #{Time.now - t} sec "
% ruby hikaku.rb

---- BimyouSegmenter
昼飯|の|スパゲティナポリタン|を|眺め|ながら|、|積年|の|疑問|を|考え|て|い|た|。|それ|は|「|なぜ|ナポリタン|は|赤い|の|だろ|う|か|」|という|問い|で|ある|。|簡単|に|見え|て|、|奥|の|深い|問題|だ|。|「|赤い|から|赤い|の|だ|」|など|と|トートロジー|を|並べ|て|悦|に|入る|浅薄|な|人間|も|いる|が|、|それ|は|思考|停止|に|他|なら|ず|、|知性|の|敗北|以外|なに|もの|で|も|ない|。|「|赤方|偏移|」|という|現象|が|ある|。|宇宙|空間|において|、|地球|から|高速|に|遠ざかる|天体|ほど|ドップラー|効果|に|より|、|その|スペクトル|線|が|赤色|の|方|に|遷移|する|という|現象|で|ある|。|つまり|、|本来|の|ナポリタン|が|何色|で|あろ|う|と|も|、|ナポリタン|が|我々|から|高速|で|遠ざかっ|て|いる|と|すれ|ば|、|毒々しく|赤く|見える|はず|な|の|だ|。|目|の|前|の|ナポリタン|は|高速|で|動い|て|いる|か|否|か|?|それ|は|ナポリタン|の|反対|側|に|回っ|て|みる|こと|で|わかる|。|運動|の|逆方|向|から|観察|する|こと|で|、|スペクトル|は|青方|遷移|し|、|青く|見える|はず|な|の|だ|。|逆|に|回っ|て|み|た|ところ|、|ナポリタン|は|赤かっ|た|。|よっ|て|この|ナポリタン|は|高速|移動|を|し|て|い|ない|と|言える|。

---- MeCab
昼飯|の|スパゲティナポリタン|を|眺め|ながら|、|積年|の|疑問|を|考え|て|い|た|。|それ|は|「|なぜ|ナポリ|タン|は|赤い|の|だろ|う|か|」|という|問い|で|ある|。|簡単|に|見え|て|、|奥|の|深い|問題|だ|。|「|赤い|から|赤い|の|だ|」|など|と|トートロジー|を|並べて|悦に入る|浅薄|な|人間|も|いる|が|、|それ|は|思考|停止|に|他|なら|ず|、|知性|の|敗北|以外|なに|もの|で|も|ない|。|「|赤|方偏|移|」|という|現象|が|ある|。|宇宙|空間|において|、|地球|から|高速|に|遠ざかる|天体|ほど|ドップラー|効果|により|、|その|スペクトル|線|が|赤色|の|方|に|遷移|する|という|現象|で|ある|。|つまり|、|本来|の|ナポリ|タン|が|何|色|で|あろ|う|とも|、|ナポリ|タン|が|我々|から|高速|で|遠ざかっ|て|いる|と|すれ|ば|、|毒々しく|赤く|見える|はず|な|の|だ|。|目|の|前|の|ナポリ|タン|は|高速|で|動い|て|いる|か|否|か|?|それ|は|ナポリ|タン|の|反対|側|に|回っ|て|みる|こと|で|わかる|。|運動|の|逆|方向|から|観察|する|こと|で|、|スペクトル|は|青|方|遷移|し|、|青く|見える|はず|な|の|だ|。|逆|に|回っ|て|み|た|ところ|、|ナポリ|タン|は|赤かっ|た|。|よって|この|ナポリ|タン|は|高速|移動|を|し|て|い|ない|と|言える|。

---- TinySegmenter(Ruby Version)
昼飯|の|スパゲティナポリタン|を|眺め|ながら|、|積年|の|疑問|を|考え|て|い|た|。|それは|「なぜ|ナポリタン|は|赤い|の|だろ|う|か|」|という|問い|で|ある|。|簡単|に|見えて|、|奥|の|深い|問題|だ|。|「|赤い|から|赤い|の|だ|」|など|と|トートロジー|を|並べ|て|悦|に|入る|浅薄|な|人間|も|いる|が|、|それ|は|思考停止|に|他|ならず|、|知性|の|敗|北以外|な|に|ものでも|ない|。「|赤方|偏移|」|と|いう|現象|が|ある|。|宇宙空間|に|おいて|、|地球|から|高速|に|遠ざかる|天体|ほど|ドップラー|効果|により|、|その|スペクトル|線|が|赤色|の|方|に|遷移|する|という|現象|で|ある|。つまり|、|本来|の|ナポリタン|が|何|色|で|あろ|うと|も|、|ナポリタン|が|我々|から|高速|で|遠ざかっ|て|いる|とすれ|ば|、|毒々しく|赤く|見える|はず|な|の|だ|。|目|の|前|の|ナポリタン|は|高速|で|動い|て|いる|か|否か|?|それは|ナポリタン|の|反|対側|に|回っ|て|みる|こと|でわかる|。運動|の|逆|方向|から|観察|する|こと|で|、|スペクトル|は|青方遷移|し|、|青く|見える|はず|な|の|だ|。|逆|に|回っ|て|み|た|ところ|、|ナポリタン|は|赤かっ|た|。よって|この|ナポリタン|は|高速移動|を|し|て|い|ない|と|言える|。

BimyouSegmenter              1000 loop 5.426601826 sec
MeCab                        1000 loop 1.153953701 sec
TinySegmenter(Ruby Version)  1000 loop 6.062332638 sec

なかなか良い感じだと思います。
分割の速度は、MeCabより5倍以上遅いです(Ruby 1.9.3の場合。1.8.7の場合は16倍以上遅い)。これはRubyで書いているせいだと思うので、Cの拡張ライブラリを書けばクソ速くなると思いますが、今はないです。

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%でした。