文字列にある文字が登場したら1、そうでなかったら0というような特徴ベクトルを束ねた行列を作ろうとしていたんだけど、巨大になりすぎた(扱うテキストの量は大分少なくしたんだけど)。つまり、こんな行列がでかくなりすぎたということだ。
グリコーゲン | 分枝 | 酵素 | 肝内 | 合成 | |
グリコーゲン分枝酵素 | 1 | 1 | 1 | 0 | 0 |
肝内グリコーゲン | 1 | 0 | 0 | 1 | 0 |
グリコーゲン合成酵素 | 1 | 0 | 1 | 0 | 1 |
完全に疎行列です、本当にありがとうございました。ということで行列の形を変更する(RubyのHashっぽく表現)。
- グリコーゲン => 1
- 分枝 => 2
- 酵素 => 3
- 肝内 => 4
- 合成 => 5
という感じでidを付けてあげると
- グリコーゲン分枝酵素 => [1,2,3]
- 肝内グリコーゲン => [4,1]
- グリコーゲン合成酵素 => [1,5,3]
という具合に持っておく情報を大幅に削減できる。まあ、グラフとかではよくある表現ですが。
Rでどうやって扱おう?
上に書いた最初の行列での持ち方をA、次のやり方をBとする。このとき、Rの分類を行えるglm(一般化線形モデル(generalized liner model)の略。ロジステック回帰などを扱える)などはAのタイプだ。が、umlsのデータと青空文庫のデータを教師データとすると、Aのやり方では教師データを吐くのに一日かかってしまいそうな勢いである(どっかやり方が悪いかもしれない)。吐いたとしてもRに載るかが結構あやしいところな気もするし。むー、Bのやり方で分類問題を扱えるRの関数とか知らないぞー(知ってる人がいたら教えてください!!)ということで、Rを使わないでどうにかする方法を模索することにする。Bの方式で扱えそうなのって、そういえばSVM-Lightとかがあったなーということを思い出した。ということでSVMが中で何をやっているかよく知らないけど、とりあえずSVMでやってみることにする。
SVM-Lightはmakeだけすれば使える感じ。これは簡単なんだが、それ用のデータを整備するのが結構面倒であった。というわけで、umlsを正例、青空文庫を負例、pneを学習したいデータとして吐き出す用のプログラムを書いた。
#include <fstream> #include <iostream> #include <sstream> #include <string> #include <map> #include <vector> #include <algorithm> #include <math.h> #include <mecab.h> #include <boost/algorithm/string.hpp> #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(); } vector<string> str2vec(string& str) { vector<string> result; MeCab::Tagger *tagger = MeCab::createTagger(""); const MeCab::Node *node = tagger->parseToNode( str.c_str() ); for( node=node->next; node->next; node=node->next ){ // 形態素解析されて出てきた文字列(品詞の情報ではなく) char *s = new char [node->length + 1]; strncpy(s, node->surface, node->length); s[node->length] = '\0'; result.push_back(s); } delete tagger; return result; } void add_substr_comb(vector<string>& strvec, vector<vector<string> >& result) { unsigned int c = 1; // いくつの大きさで見ていくか // cout << c << " : " << strvec.size() << endl; while (c <= strvec.size()) { for (size_t i = 0; i < strvec.size() - c + 1; ++i) { vector<string> tmp; vector<string>::iterator it = strvec.begin(); advance(it, i); vector<string>::iterator start = it; advance(it, c); vector<string>::iterator end = it; tmp.assign(start, end); result.push_back(tmp); } c++; } } struct map_for_feature_vector { map<string, int> str2id; // ⽂字列から素性番号への連想配列 vector<string> id2str; // 素性番号から⽂字列への逆引き 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() + 1; 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> > positive_example; vector<vector<string> > negative_example; // 教師データの作成(正例) std::ifstream fis("/Users/syou6162/dbcls/umls/umls2ja.tab"); string str; while (getline(fis, str)) { vector<string> strvec; boost::algorithm::split(strvec, str, boost::algorithm::is_any_of("\t")); vector<string> tmp = str2vec(strvec[2]); positive_example.push_back(tmp); for (vector<string>::iterator it = tmp.begin(); it != tmp.end(); ++it) { m.getID(*it); } } // 教師データの作成(負例) string dir = "/Users/syou6162/dbcls/aozora/"; path fullPath = complete(path(dir, native)); directory_iterator end; int i = 0; for (directory_iterator it(fullPath); it !=end; ++it){ 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) { negative_example.push_back(*vecitr); for (vector<string>::iterator stritr = (*vecitr).begin(); stritr != (*vecitr).end(); ++stritr) { m.getID(*stritr); } } i++; if (i > 10) { break; } } // データの表示(正例) // for (vector<vector<string> >::iterator vecitr = positive_example.begin(); vecitr != positive_example.end(); ++vecitr) { // cout << 1 << " "; // vector<int> feature_vector; // for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) { // feature_vector.push_back(m.getID(*strit)); // } // sort(feature_vector.begin(), feature_vector.end()); // feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end()); // for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) { // cout << *strit << ":" << 1 << " "; // } // cout << endl; // } // データの表示(負例) // for (vector<vector<string> >::iterator vecitr = negative_example.begin(); vecitr != negative_example.end(); ++vecitr) { // cout << -1 << " "; // vector<int> feature_vector; // for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) { // feature_vector.push_back(m.getID(*strit)); // } // sort(feature_vector.begin(), feature_vector.end()); // feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end()); // for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) { // cout << *strit << ":" << 1 << " "; // } // cout << endl; // } // 学習したいデータ vector<vector<string> > test_data; 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){ 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) { add_substr_comb(*vecitr, test_data); } } } for (vector<vector<string> >::iterator vecitr = test_data.begin(); vecitr != test_data.end(); ++vecitr) { string s; s = accumulate((*vecitr).begin(), (*vecitr).end(), s); cout << s << endl; } // データの表示(テストデータ) // for (vector<vector<string> >::iterator vecitr = test_data.begin(); vecitr != test_data.end(); ++vecitr) { // cout << 0 << " "; // vector<int> feature_vector; // for (vector<string>::iterator strit = (*vecitr).begin(); strit != (*vecitr).end(); ++strit) { // feature_vector.push_back(m.getID(*strit)); // } // sort(feature_vector.begin(), feature_vector.end()); // feature_vector.erase(unique(feature_vector.begin(), feature_vector.end()), feature_vector.end()); // for (vector<int>::iterator strit = feature_vector.begin(); strit != feature_vector.end(); ++strit) { // cout << *strit << ":" << 1 << " "; // } // cout << endl; // } return 0; }
いくつかはまったところがあって
- 特徴のidは0以上にしろよ!!
- 特徴はソートしてから書いておけよ!!
- 学習したいやつのラベルは0にしておけよ!!
などなどというところにつまづきつつ、とりあえずできた。で、SVM-Lightを走らせる。
/Users/syou6162/dbcls/src/cpp% ../../../Downloads/svm_light/svm_learn hoge.txt model
/Users/syou6162/dbcls/src/cpp% ../../../Downloads/svm_light/svm_classify test.txt model prediction
SVMが内部でどういうことをやっているとか、パラメータの推定に何使っているとかは今勉強している暇ないので、とりあえず全部ディフォルト。
で、predictionっていうファイルに予測結果が出てくる。こんなの。このファイルをCとしよう。
/Users/syou6162/dbcls/src/cpp% head prediction 0.96817413 1.7556166 1.7873854 0.93640533 0.93640533 1.0745405 1.8900646 0.93640533 -0.17596597 0.93640533
0以上だったら、(ここでは)専門用語と判定されたもの、そうでなかったら普通の単語として判定されたものである。で、元の単語が何だったかがこれだと分からないので、吐き出すスクリプトも用意(さっきの上のやつに組み込んでいたんだった)。
/Users/syou6162/dbcls/src/cpp% head name.txt 貯蔵 蛋白 貯蔵蛋白 セプター エクダイソン 活性 化 センチニクバエ 三 令
吐き出された名詞が入っているファイルをDとしよう。で、CとDのファイルをマージすればいいんだけど、ワンライナーでできそうなやり方が分からん。まあ、とりあえずRubyで書くかーということで書いた。どうせだから、スコア(というか距離?)順にソートするようにしておいた。zip関数とかid:Hashが書いてたなーと思って初めて使ってみた。
require "pp" name = [] f = File.open("../cpp/name.txt", "r") f.each{|line| name.push line.chomp } f.close prediction = [] f = File.open("../cpp/prediction", "r") f.each{|line| prediction.push line.chomp.to_f } f.close name.zip(prediction).sort{|a, b| (b[1] <=> a[1]) * 2 + (a[0] <=> b[0]) }.each{|x| puts "#{x[0]} : #{x[1]}" }
で、スコアの高いほう(専門用語であろうと判定されたもの)を見ていくとこんな感じになっている。お、わりと専門用語っぽい単語が抽出されているし、専門用語じゃないやつははじけてるっぽい!!
性蛋白質(AdcodedDNAbindingprotein;AdDBP : 7.4176569 -5'-リン酸依存性酵素 : 7.4066807 ピリドキサル-5'-リン酸依存性酵素 : 7.4066807 DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6- : 7.4048364 メルカプツール酸(N-アセチルシステイン抱合体) : 7.3970092 酸(N-アセチルシステイン抱合体) : 7.3970092 固定化コレステロール酸化酵素(COD)膜 : 7.3960023 酢酸緩衝液(pH6.0)(5)Somogyi-Nelson試薬 : 7.393121
逆にスコアの低いほう(専門用語ではなかろうと判定されたもの)を見てみる。今度は専門用語っぽいものは入ってなくて、一般の名詞が多いということが分かる。
1例以来,1985年4 : -5.6792571 1例以来,1985年 : -5.6807515 以来,1985年4月30 : -5.6824445 例以来,1985年4 : -5.7144712 以来,1985年4月 : -5.7147269 例以来,1985年 : -5.7159655 ,1985年4月30日 : -6.2655624 1985年7月26日 : -6.3123316 1985年4月30日 : -6.3741314 1例以来,1985年4月30 : -7.5829741 1例以来,1985年4月 : -7.6152565 以来,1985年4月30日 : -7.6179166 例以来,1985年4月30 : -7.6181882 例以来,1985年4月 : -7.6504705 1例以来,1985年4月30日 : -9.5184462 例以来,1985年4月30日 : -9.5536603
現在使っている特徴はものすごく単純というか、SVMできる最低限のものという感じだからもちろんうまくいっていない部分もある。例えばこういうものだ。
性口内炎ウイルス感染BHK細胞 : 10.727399 活性(DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6-リン酸 : 10.515023 (DNA,蛋白質合成,アミノ酸透過,コレステロール合成,グルコース6-リン酸 : 10.376888 ,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.84303 ,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.84303 ,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.84303 ,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.84303 (1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.734461 (1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.734461 (1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.734461 (1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.734461 1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.734461 1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.734461 1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.734461 1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.734461 )固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性 : 9.699247 )固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素 : 9.699247 )固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素- : 9.699247 )固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極 : 9.699247
分類したいデータのほうでは、「,(1)固定化酵素膜-電極(2)固定化酵素カラム-電極(3)水溶性酵素-電極」みたいな一番長い名詞から、MeCabで区切らせたのを要素として、全部の部分文字列の組み合わせのようなものを考えている(この辺を参考)。この中でよいの(「固定化酵素カラム-電極」とか)を選んでくれればいいんだけど、その付近ができていなくて全部のスコアが同じくらい、ということになっている(ということで、スコアの高いものはこういう同じような単語が出現しまくっている状況)。前後の文脈とかまだ入れてないのもあるし、その付近かなあ。複合名詞にある単語が出現したかどうかの0、1だけではまあこんなもんか。
というわけで今週は単語自身の素性だけじゃなくって文脈の素性を使ってどうにかできるようにしようと思います。