分類問題のための教師データの作成

6月の間は出現頻度と連接頻度に基づく専門用語抽出にある論文の考え方にそって、C++とかRubyで実装していた。しかし、どうも精度や再現率が低いid:theclaさんに結果を見せてみたところ「おしいのは結構あるんだけどねえ」という感じで、(精度とかを測る時に)単語の完全一致で見るからから低いのかな…という感じ。どの辺がネックになっているかというと、まあ本当に色々あって、一概には言えないんだけど、化学式がネックになっているところが結構ある。化学式を専門用語に含めるかどうかってところの問題もあるんだけど、umlsには化学式のものもかなり大量に含まれていて、とりあえず化学式も専門用語に含む方向で。化学式だと、普通の分野では複合名詞に入ってこないような

  • ,
  • `
  • ()

といったものが入ってくるのが厄介なところである。ある程度は「これこれこういう場合は除去して…」とかルールベースでやってみたんだけど、やらないといけない数が多すぎるし、俺が化学とか高校以来やってないから、どういうものが出てくるかとか激しく忘れているしというのもあって、この方向で進めるのはひとまずストップすることにした。この方法も漢字、ひらがな、カタカナとかからなるような複合名詞に対してはうまく動きそうなんだけどねえ。

機械学習でやってみる

ルールベースだと(化学式のような)例外の列挙が大変なので、機械学習でどうにかしよう。ちょうど今PRML4章の線形識別モデルも勉強しているところだし。複合名詞が与えられて、専門用語 or notの二値に分類する問題として取り扱うことにする。とりあえず

  • どういうものを素性として扱えばいいか
  • それが定義できたとして、どうやって作るか

の知識やノウハウがないため、論文とかBlogなどを漁ってみる作業を開始。この辺とかが参考になりそう。

例えば「グリコーゲン分枝酵素」という複合名詞があったときに、「グリコーゲン」、「分枝」、「酵素」という3つの名詞に分けて、それぞれが出てきたら1、出てこなかったら0というのを特徴ベクトルとする、というのがよさそうである(つまり、単名詞でフラグを立てる)。

C++でこんな感じで書いてみた。

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <map>
#include <vector>
#include <algorithm>
#include <math.h>
#include <mecab.h>
#include <boost/filesystem/operations.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include "wrapper.cpp"

using namespace std;
using namespace boost;
using namespace boost::filesystem;

string itos(int i) {
  // convert int into string
  ostringstream s;
  s << i;
  return s.str();
}

struct map_for_feature_vector {
  map<string, int> str2id; 
  // &#12098;字列から素性番号への連想配列 
  vector<string> id2str; 
  // 素性番号から&#12098;字列への逆引き 
  int getID(const string& str){ 
	map<string, int>::const_iterator it = str2id.find(str); 
	if (it != str2id.end()){ 
	  return it->second;   // 登録済みのIDを返す 
	} else {               // 登録されてない 
	  const int newID= str2id.size(); 
	  str2id[str] = newID; // 登録 
	  id2str.push_back(str); 
	  return newID;        // 新しいiDを返す 
	} 
  }
  int size () {
	return str2id.size();
  }
};

int main (int argc, char **argv) {
  map_for_feature_vector m;
  vector<vector<string> > strvec;
  for (int year = 1985; year < 1986; ++year) {
	string dir = "/Users/syou6162/dbcls/pne/" + itos(year);
	path fullPath = complete(path(dir, native));
	directory_iterator end;
	for (directory_iterator it(fullPath); it !=end; ++it){
	  cout << (dir + "/"+ it->leaf()).c_str() << endl;
	  std::ifstream fis((dir + "/" + it->leaf()).c_str());
	  string str;
	  char c;
	  while (fis.get(c)) {
		str.push_back(c);
	  }
	  MeCab::Tagger *tagger = MeCab::createTagger("-O wakati");
	  MeCabWrapper mw(tagger);
	  mw.addText(str);
	  delete tagger;
	  for (vector<vector<string> >::iterator vecitr = mw.extraction.begin(); vecitr != mw.extraction.end(); ++vecitr) {
		strvec.push_back(*vecitr);
		for (vector<string>::iterator stritr = (*vecitr).begin(); stritr != (*vecitr).end(); ++stritr) {
		  m.getID(*stritr);
		}
	  }
	}
  }
  return 0;
}

教師データの特徴

ここでの教師データはumlsやlsd(ライフサイエンス辞書)にあるデータとなるのだが、これは辞書の形なので、前後の文脈がない。つまり

  • 分類したい複合名詞の(前|後ろ)の単語の
    • 品詞
    • 単語自体

などを教師側のデータとして持たせておくことができない。と思ったけど、そうではないかも。「分枝酵素」という単語がumlsの中にあったときに、pne(蛋白質核酸酵素)のテキスト側の情報を見てあげれば、辞書だけでは分からない情報を埋めることができそうである。俺、good idea!!

ただし、これをやろうと思うとすごい量のテキストに対して、すごい量の単語で検索しないといけなくなるので、工夫しないとどうにもならなくなりそうである。とりあえずこれなしでやってみて、うまくいかなかったらこれも取り入れてみる、という方向で行ってみるかな。

教師データの整備

専門用語で(ある|ありそう)なデータは、手元にlsd、umls、pneという形のデータで揃っているから、これは加工すればどうにかなる。が、教師側のデータで不例となるものが手元にはない。不例のデータというのは、専門用語ではないような一般的な用語(特に名詞)である。ということで、一般的な文章はないかなーということで探すことにした。新聞のデータとかが手に入るとよかったんだけど、契約しないと触れなかったりするので、今回は青空文庫のデータを使ってみることにした。

青空文庫のデータは全部で8000個くらいあって、zipファイルで固められているので、そこから欲しいテキストを取ってくるスクリプトを書いた。

# -*- coding: utf-8 -*-
require 'rubygems'
require 'zipruby'
require 'open-uri'
require 'kconv'

path = "/Users/syou6162/Downloads/www.aozora.gr.jp/cards/"
aozora = "/Users/syou6162/dbcls/aozora/"

Dir.glob(path + "**/*.zip").each{|file|
  output_file = File.open(aozora + file.split("/").pop.sub("zip", "txt"), "w")
  open(file) do | zipFile |
    Zip::Archive.open_buffer(zipFile.read) do | ar |
      ar.fopen(ar.get_name(0)) do | f |
        output_file.puts f.read.toutf8
      end
    end
  end
  output_file.close
}

実行する前に

sudo gem install zipruby

が必要。

これから問題になりそうなこと

  • 何のモデルで回すか?
    • ロジステック回帰?SVM?
  • 何で動かすか?
    • 一番使いなれているのはR。だけど、数十万な次元でまともに動いてくれるかが少し不安。次元を削減でもすればいいのかなあ。。。
    • C++とかで、自分で書いてみるのもいいけど、できるかどうか
  • lsdやumlsのデータに対して過学習しそうな気がする
    • 正則化すればなんとかなる?