ナイーブベイズで「日本」の読み分けを試す

はじめに

「日本」は、「にほん」と「にっぽん」どちらの読み方もできる。
しかし、読み分けが必要な場合も存在する。(東京の日本(にほん)橋と大阪の日本(にっぽん)橋、会社名、など)

同型異音語や多義性解消だと、よく周辺文字を素性にして分類問題を解く、
というアプローチが取られるよう(参考文献)なので、この読み分けをナイーブベイズで試してみる。

アプローチ

  • ここでは、単語「日本」の前後のn文字ずつを使って読み分けてみる
    • 文字cとして、文は「c_0 ... c_{n-1} 日本 c_{n+1} ... c_{2n}」
    • 「周辺文字を素性にした分類問題」とみなせる

データの準備

学習および評価用のデータはwikipediaの文を使う。

wikipediaページのダウンロード
フィルタ

以下が含まれるものは除いた

  • 日本+格助詞(の、が、を、に、へ、と、より、から、で)
  • 「日本語」
  • 「日本人」
  • 「日本]」
  • 「日本)」
  • 日本+記号など(読点、]、)、スペース)

また、「にほん」「にっぽん」以外の読み方のものは除いた

記号系・複数含む行の削除
正解基準
  • 会社ページがあればそれを優先
  • 基本的に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%
のようです。