簡単なラティス構築とビタビアルゴリズム
説明
- 入力文に対し、部分文字列に対し辞書引きをして、ノードとする
- エッジは文字列上で隣接している場合のみそのノード間をつなぐ
- 文頭(BOS)、文末(EOS)という特殊なノードを追加し、グラフ(ラティス構造)を作成
- 各ノードおよびエッジにはコストが定められており、BOSからEOSまでのパスで、通ったノード・エッジの総コストが最小なものを選ぶ
- DP(ビタビアルゴリズム)で各頂点までの最小コストを保持すればいい
コード
- 以下、[TODO]で変更すべき点をコメントしとく
#include <iostream> #include <vector> #include <string> #include <algorithm> const static int COST_MAX = 100000000; //グラフの頂点を表す構造体([TODO]読み、品詞などの情報を追加) struct NODE { int start_pos; std::string word; int score; NODE* prev; NODE(int start_pos, std::string word):start_pos(start_pos),word(word){ score = 0; prev = NULL; } }; //辞書引き([TODO]double arrayなどで探す) std::vector<std::string> dic_lookup(const std::string & word){ std::vector<std::string> ret; //[TODO]読みや品詞の情報付きで返すようにする if(word=="子猫") ret.push_back("子猫"); if(word=="猫") ret.push_back("猫"); if(word=="が") ret.push_back("が"); if(word=="もふ") ret.push_back("もふ"); if(word=="もふもふ") ret.push_back("もふもふ"); if(word=="して") ret.push_back("して"); if(word=="してい") ret.push_back("してい"); if(word=="いる") ret.push_back("いる"); return ret; } //ラティス構造を作成([TODO]common prefix searchなどで効率的に構築) std::vector< std::vector<NODE> > graph_construct(const std::string& str){ std::vector< std::vector<NODE> > graph(str.length()+2, std::vector<NODE>()); graph[0].push_back(NODE(0,"BOS")); graph[str.length()+1].push_back(NODE(str.length()+1,"EOS")); for(int i=0; i<str.length(); i++){ for(int j=i+1; j<str.length()+1; j++){ std::string sbstr = str.substr(i,j-i); std::vector<std::string> hret = dic_lookup(sbstr); for(int k=0; k<hret.size(); k++){ graph[j].push_back(NODE(i+1,hret[k])); } } } return graph; } //nodeの生起コスト int get_node_cost(NODE &node){ return 0; //[TODO]コストを与える } //node_aからnode_bへの連接コスト int get_edge_cost(NODE &node_a, NODE &node_b){ return 0; //[TODO]コストを与える } //ビタビアルゴリズムによるコスト最小法 std::vector<std::string> viterbi(std::vector< std::vector<NODE> > &graph){ int n = graph.size(); for(int i=1; i<n; i++){ for(int j=0; j<graph[i].size(); j++){ int node_cost = get_node_cost(graph[i][j]); int cost = COST_MAX; NODE *shortest_prev = NULL; if(graph[graph[i][j].start_pos-1].size()==0) continue; for(int k=0; k<graph[graph[i][j].start_pos-1].size(); k++){ int edge_cost = get_edge_cost(graph[graph[i][j].start_pos-1][k], graph[i][j]); //int tmp_cost = get_node_cost(graph[graph[i][j].start_pos-1][k]) + edge_cost + node_cost; //これは誤り int tmp_cost = graph[graph[i][j].start_pos-1][k].score + edge_cost + node_cost; //修正(2017/8/19): コメントを参照 if(tmp_cost < cost){ cost = tmp_cost; shortest_prev = &graph[graph[i][j].start_pos-1][k]; } } graph[i][j].prev = shortest_prev; graph[i][j].score = cost; } } NODE *node = &graph[n-1][0]; //EOS std::vector<std::string> ret; while(node->word != "BOS"){ ret.push_back(node->word); node = node->prev; if(node == NULL) return std::vector<std::string>(); //失敗 } std::reverse(ret.begin(), ret.end()); return ret; } int main(){ using namespace std; //ラティスを作成 vector< vector<NODE> > graph = graph_construct("猫がもふもふしている"); //ビタビアルゴリズムでコストが最小となるパスを検索 vector<string> ret = viterbi(graph); //結果の表示 for(int i=0; i<ret.size(); i++){ cout << ret[i] << endl; } return 0; }
実行結果
$ ./a.out 猫 が もふもふ して いる EOS
参考文献
- WEB+DB press 2011 vol.64「作って学ぶ日本語入力」
- http://d.hatena.ne.jp/nokuno/20110901/1314829775
追記(2017/8/19)
viterbi()関数の最初の3重ループの中は以下のようになっている。
注目しているノードgraph[i][j](図の緑ノード)について、そのノードにつながっている一つ前の各ノードgraph[graph[i][j].start_pos-1][k](図の左側ノードのどれか)を通った場合の総コスト(tmp_cost)を計算し、最後にそのなかで最小の総コスト(cost)をノードgraph[i][j].scoreに保存しておく。
ひとつ前のノードでの最小の総コスト(graph[graph[i][j].start_pos-1][k].score)がわかっていれば、そのノードを通って注目しているノードgraph[i][j]に来た場合の総コスト(tmp_cost)は、それにgraph[i][j]へのエッジのコストとgraph[i][j]のノードのコストを足すことで計算することができる。
最後に、EOSのノードでの最小総コストを確認すれば、BOSからEOSまでの最小総コストがわかり、どこを通ったか(各ノードのprev)をEOSから逆にたどれば最小総コストになる経路を復元できる。