ナイーブベイズで「日本」の読み分けを試す
はじめに
「日本」は、「にほん」と「にっぽん」どちらの読み方もできる。
しかし、読み分けが必要な場合も存在する。(東京の日本(にほん)橋と大阪の日本(にっぽん)橋、会社名、など)
同型異音語や多義性解消だと、よく周辺文字を素性にして分類問題を解く、
というアプローチが取られるよう(参考文献)なので、この読み分けをナイーブベイズで試してみる。
アプローチ
- ここでは、単語「日本」の前後のn文字ずつを使って読み分けてみる
- 文字cとして、文は「c_0 ... c_{n-1} 日本 c_{n+1} ... c_{2n}」
- ngramや単語や形態素などでも
- 「周辺文字を素性にした分類問題」とみなせる
- 文字cとして、文は「c_0 ... c_{n-1} 日本 c_{n+1} ... c_{2n}」
データの準備
学習および評価用のデータはwikipediaの文を使う。
wikipediaページのダウンロード
- wikipediaのページのデータから、ランダムに500件取り出す
- http://dumps.wikimedia.org/jawiki/latest/
- jawiki-latest-pages-articles.xml.bz2
- (使ったのは20120826のもの)
- 500件までやって飽きt(爆破
フィルタ
以下が含まれるものは除いた
- 日本+格助詞(の、が、を、に、へ、と、より、から、で)
- 「日本語」
- 「日本人」
- 「日本]」
- 「日本)」
- 日本+記号など(読点、]、)、スペース)
また、「にほん」「にっぽん」以外の読み方のものは除いた
正解基準
- 会社ページがあればそれを優先
- 基本的にwikipedia読み優先、複数ある場合は1つ目の方を採用
- Webページに読みがあれば、上記で見つからない場合はそれを採用
- よく読みがわからないものは「にほん」によせている
正解データ
作ったデータは、以下。
https://github.com/jetbead/Prog/tree/master/naiveBayes/data
コード
以前作ったコードの使い回し。
(logとって計算してないし...)
#include <algorithm> #include <iostream> #include <fstream> #include <vector> #include <map> #include <set> using namespace std; //教師情報付きドキュメント struct Document { int class_no; //クラスの番号 set<string> words; //出現する単語リスト void add(string word){ words.insert(word); } }; //ナイーブベイズ(多変数ベルヌーイモデル、MAP推定、alpha=2) class NaiveBayes { int m_num_of_class; set<string> m_all_words; vector<int> m_num_docs; vector< map<string,int> > m_class; int m_num_of_all_docs; public: //コンストラクタ NaiveBayes(int num_of_class){ m_num_of_class = num_of_class; m_num_of_all_docs = 0; for(int i=0; i<m_num_of_class; i++){ m_num_docs.push_back(0); m_class.push_back(map<string,int>()); } } //訓練関数 void train(const Document& docs){ if(docs.class_no < 0 || docs.class_no >= m_num_of_class){ cerr << "invalid train-data." << endl; return; } m_num_docs[docs.class_no]++; m_num_of_all_docs++; set<string>::const_iterator itr = docs.words.begin(); for(; itr != docs.words.end(); ++itr){ m_class[docs.class_no][(*itr)]++; m_all_words.insert(*itr); } } //予測関数 int predict(vector<string>& words){ vector<double> ret(m_num_of_class, 0.0); const double m_alpha = 2; for(int c=0; c<m_num_of_class; c++){ double Pc = (m_num_docs[c] + (m_alpha - 1.0))/((double)m_num_of_all_docs + m_num_of_class * (m_alpha - 1.0)); double Pwc_all = 1.0; set<string>::iterator itr = m_all_words.begin(); for(; itr != m_all_words.end(); ++itr){ double Pwc = (m_class[c][*itr] + (m_alpha - 1.0))/((double)m_num_docs[c] + 2 * (m_alpha - 1.0)); if(find(words.begin(), words.end(), *itr) != words.end()){ //出てきた場合 Pwc_all *= Pwc; }else{ //出てこなかった場合 Pwc_all *= (1.0-Pwc); } } ret[c] = Pc * Pwc_all; } int ret_idx = -1; double maxP = -1.0; for(int i=0; i<m_num_of_class; i++){ //cerr << "class " << i << ": " << ret[i] << endl; if(maxP<ret[i]){ maxP = ret[i]; ret_idx = i; } } return ret_idx; } }; int main(int argc, char** argv){ fstream trainf(argv[1]); fstream testf(argv[2]); NaiveBayes nb(2); int c_type; string str; //学習 while(trainf >> c_type){ int num; trainf >> num; Document d; d.class_no = c_type; for(size_t i=0; i<num; i++){ trainf >> str; d.add(str); } nb.train(d); } //予測 int success = 0, failure = 0, allnum = 0; int c_zero_num = 0; while(testf >> c_type){ int num; testf >> num; vector<string> wrds; for(size_t i=0; i<num; i++){ testf >> str; wrds.push_back(str); } allnum++; if(c_type == nb.predict(wrds)){ success++; }else{ failure++; } if(c_type == 0){ c_zero_num++; } } //結果 cout << "Acc : " << (success*100.0/allnum) << "% "; cout << "(0: " << c_zero_num << " / " << allnum << ")" << endl; return 0; }
結果
「日本」前後の5文字を使って、5交差してみる。
(%数値がテストデータの正解率で、括弧内がテストデータ内の「にほん」の個数 / テスト件数)
$ ./a.out data/sosei.2345.txt data/sosei.1.txt Acc : 82% (0: 77 / 100) $ ./a.out data/sosei.1345.txt data/sosei.2.txt Acc : 87% (0: 84 / 100) $ ./a.out data/sosei.1245.txt data/sosei.3.txt Acc : 93% (0: 87 / 100) $ ./a.out data/sosei.1235.txt data/sosei.4.txt Acc : 85% (0: 82 / 100) $ ./a.out data/sosei.1234.txt data/sosei.5.txt Acc : 79% (0: 74 / 100)
正解データ作ってて思ったけど、組織名などあまりかぶりがなかったので、500件ぐらいじゃ学習できてなさそう。
(平均5個ぐらいは当てられてるみたいだけど、、、)
もっと学習データを集めるのもあるけど、固有名詞がメインなので、結局、直接事例を集めてルールでやるのがよさそうかも。
追記(2013/4/30):
正解率のマクロ平均は、5セットで、
1/5 * (0.82+0.87+0.93+0.85+0.79) = 0.852 = 85%
のようです。