Try for Trie
-
Upload
toshiyuki-maezawa -
Category
Documents
-
view
2.776 -
download
3
Transcript of Try for Trie
TRIE にトライ!~今日からはじめる TRIE 入門~
echizen_tmJuly 23, 2011
目次
● 自己紹介 (1 slides)● 第 1 章 TRIE とは (3 slides)● 第 2 章 TRIE を作ってみた (25 slides)● 第 3 章 LOUDS とは (13 slides)● 第 4 章 LOUDS を作ってみた (7 slides)● まとめ (1 slides)● 参考資料 (2 slides)
自己紹介 (1/1)
● ID : echizen_tm● ブログ: EchizenBlog-Zwei
● 職業: web エンジニア● 興味:簡潔データ構造、機械学習● お仕事:コンテンツマッチ、レコメンデーション、
スペルコレクション
● 最近はまっていること: Project Euler
第 1 章TRIE とは
TRIE とは (1/3)
● Edward Fredkin が提案 (1960)● 木構造の一種
● どんなことができるの?● キーワードに対する値を返す (Key-Value Store)● キーワードに前方一致する別のキーワードを獲得する
– 言語 → 言語処理、言語学、言語資源– ロウ → ロウ材、ロウレイズティー、ロウきゅーぶ!
● ライブラリとかあるの?● darts(@taku910 さん ) 、 tx-trie(@hillbig さん ) 、
marisa-trie(@s5yata さん ) など
TRIE とは (2/3)
● TRIE の良いところ● KVS よりリッチな操作ができる● 検索が高速(辞書サイズに対して O(1) )
● TRIE の悪いところ● メモリをたくさん使う
( 素直に実装すると辞書サイズに対して O(NlogN))
実用的なライブラリではデータサイズを小さくするアルゴリズムが使われている
TRIE とは (3/3)
● どんなアルゴリズムがあるの?● Patricia Tree ( パトリシア木 )
– 子ノードが一つしかない部分をまとめて一つのノードに● Double Array ( ダブル配列 )
– base 、 check という二本の配列で TRIE を表現– darts で使われている
● LOUDS ( ラウズ、 Level-Order Unary Degree Sequence)– 簡潔データ構造を使って圧縮– tx-trie や marisa-trie で使われている
● XBW Transform(X Burrows-Wheeler 変換 )– 文字列の圧縮法である BWT を木構造に拡張
第 2 章TRIE を作ってみた
TRIE を作ってみた (1/25)
● 理解を深めるために TRIE を作ってみた
● その名も erika-trie !
● ちなみに前回はCompressed Suffix Array ライブラリ tsubomiを作った
TRIE を作ってみた (2/25)● ちなみに他のライブラリと並べてみると
● そんなに違和感がない! ・・・気がする
darts tx-trie
marisa-trie erika-trie
TRIE を作ってみた (3/25)
● 気をとりなおして実装の解説
● まずは実装の方針を● C/C++で作る● まずはシンプルに● 実用性はとりあえず後回し● 気になった部分に適宜、手を入れていく
● とにかく勉強のために作る!
TRIE を作ってみた (4/25)
まずは必要な機能を考える
TRIE を作ってみた (5/25)● TRIE に必要な機能は以下のとおり● TRIE を構築する
● 用意した辞書(キーワードと値のペアのリスト)からTRIE 木を作る
● アルゴリズムによって適切な構築法が異なる
● TRIE で検索する● 入力キーワードに一致(もしくは前方一致)する
キーワードを TRIE 木から探す● キーワードと値のペアを返す
● TRIE をファイルに書き出す● TRIE をファイルから読み込む
TRIE を作ってみた (6/25)● 複数の TRIE(普通のと LOUDS) を実装したい● 以下のような抽象クラスを作った
● 実際の TRIE は派生クラスで実装● search() 、 read() 、 write() のみ共通機能● 構築は TRIE毎に個別
● class trie {public: virtual void search(const string &key, vector<pair<string, string>> &value) = 0; virtual bool read(const char *filename) = 0; virtual void write(const char *filename) = 0;};
TRIE を作ってみた (7/25)● まずは普通の TRIE から● 以下のような basic_trie クラスを作った
● 抽象クラス trie を継承● キーワードと値のペアを追加する add() を用意● add() を繰り返して TRIE を構築
● class basic_trie : public trie {public: void add(const string &key, const string &value); void search(const string &key, vector<pair<string, string>> &value); bool read(const char *filename); void write(const char *filename);};
TRIE を作ってみた (8/25)
次にデータ構造について考える
TRIE を作ってみた (9/25)● 根から葉までたどるとキーワードができる● 葉はキーワードに対する値を持っている
ア
ラ
カ
イ
ドス ス
ル20
15 10
TRIE を作ってみた (10/25)● ノードのラベルについて考える
● 固定長 ( ラベル = 1文字 )● 子ノードのサイズが固定 ( 文字の種類数 )
→固定サイズの配列を作れば定数時間でアクセス可
● 可変長 ( ラベル =任意文字列 )● Patricia 木に拡張しやすい● 子ノードのサイズが不定
→線形探索or辞書順ソートして二分探索でアクセス可
● 拡張性を考えて可変長にした● ラベルは UTF8の 1 文字。子ノードは線形探索
TRIE を作ってみた (11/25)
● ノードは以下のクラスで表現した● ノードのラベルは string型● ノードが葉かどうかは bool型のフラグで管理
● class node {public: string label_; bool is_value_;
node(const string &, bool); ~node();};
TRIE を作ってみた (12/25)● ノードと子ノードのリストを
まとめて element クラスで表現した● 子ノードは int型の elementID で管理● 複数子ノードがあるので vector型で持つ● element の vector で TRIE全体を表す
(↑の添字が element の ID )
● class element {public: node n_; vector<int> a_;
element(const node &); ~element();};
TRIE を作ってみた (13/25)● 変数の部分だけまとめると以下のようになる
● class basic_trie { vector<element> g_;};
● class element { node n_; vector<int> a_;};
● class node { string label_; bool is_value_;};
TRIE を作ってみた (14/25)
データ構造が決まったのでTRIE の構築を考える
TRIE を作ってみた (15/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する
ア
イ
ス
20
アイス: 20アイス: 20
TRIE を作ってみた (16/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する
ア
ラ
カ
イ
ス ス
20
10
アラスカ: 10アラスカ: 10
TRIE を作ってみた (17/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する
ア
ラ
カ
イ
ドス ス
ル20
15 10
アイドル: 15アイドル: 15
TRIE を作ってみた (18/25)● 具体的にはこんな感じ● for (j = 0; j < key.size(); j++) {
iterator i = g_[pos].a_.begin(); iterator e = g_[pos].a_.end(); while (i != e) { if (g_[*i].n_.is_value_ == false && g_[*i].n_.label_ == key[j]) { pos = *i; break; } } i++; } if (i == e) { g_[pos].a_.push_back(g_.size()); g_.push_back(element(node(key[j]))); }}
キーワードを1 文字ずつチェック
子ノードを線形探索
見つからなかったらノード追加
TRIE を作ってみた (19/25)
TRIE を構築したので検索について考える
TRIE を作ってみた (20/25)● まずは単純に入力キーワードを探索● 見つからなかったらそこで終了
ア
ラ
カ
イ
ドス ス
ル20
15 10
アイアイ
TRIE を作ってみた (21/25)● 入力キーワードを見つけた場所のノードに対して
子ノードを DFS(縦型探索 ) してキーと値を回収
ア
ラ
カ
イ
ドス ス
ル20
15 10
アイアイ
アイス: 20アイス: 20アイドル: 15アイドル: 15
TRIE を作ってみた (22/25)● 具体的にはこんな感じ
● for (j = 0; j < key.size(); j++) { iterator i = g_[pos].a_.begin(); iterator e = g_[pos].a_.end(); while (i != e) { if (g_[*i].n_.is_value_ == false && g_[*i].n_.label_ == key[j]) { pos = *i; break; } } i++; } if (i == e) { return }}retrieve(pos, key, values);
キーワードを1 文字ずつチェック
子ノードを線形探索
見つからなかったらそこで終わり
見つかったら縦型探索して値を回収
TRIE を作ってみた (23/25)
● 探索について
● DFS(縦型探索、深さ優先探索 )● スタックで実装● 関数の再帰呼び出しで代用可● 木の階層が浅い場合に有効
● BFS(横型探索、幅優先探索 )● キューで実装● 木のノード数が少ない場合に有効
● TRIE は木が浅くてノードが多いので DFS を使った
TRIE を作ってみた (24/25)● ちなみに・・・● 探索アルゴリズムを学ぶにはアリ本
(プログラミングコンテスト チャレンジブック )が超オススメ!● コードが豊富に掲載されている (C/C++ 。 STL多め )● 状況に応じたアルゴリズムの使い方が学べる● 数論に興味が出てくる
● 持ってない人はいますぐ購入しよう!
TRIE を作ってみた (25/25)
ここまでで普通の TRIE が完成!
次はLOUDS に拡張するよ!!
第 3 章LOUDS とは
LOUDS とは (1/13)
● G. Jacobson が提案 (1989)● Level-Order Unary Degree Sequence の略
● 構築済み TRIE から LOUDS を構築する● 作業領域が O(NlogN) から O(N) に
● 具体的には子ノードの ID リストではなく子ノードの数だけ持っていれば良くなった
● 簡潔データ構造によって子ノードの ID が定数時間で計算可能
● ただしノードの追加、削除は不可
LOUDS とは (2/13)
では具体的にLOUDS 構築の手順を見てみよう!
LOUDS とは (3/13)● ノードに幅優先探索の順番 (Level-Order)
で番号をつける
0
21
7
43 5
8
ア
ラ
カ
イ
ドス ス
ル6 20
9 15 10 10
LOUDS とは (4/13)● それぞれの子ノードの数 (Degree) を数える● 元論文ではこの部分を Unary符号で実装していた
(2)
(0)
0
21
7
43 5
8
ア
ラ
カ
イ
ドス ス
ル6 20
9 15 10 10
(2) (1)
(1)
(1)
(1)
(1)
(1)
(0)(0)
LOUDS とは (5/13)● Level-Order で子ノード数 (Unary Degree) を
並べたデータ列 (Sequence) をつくる
(2)
(0)
0 21
7
43 5
8
ア ラ
カ
イ ドス ス
ル
6
20
9
15
10
10
(2) (1) (1)
(1)
(1)
(1)
(1)
(0)(0)
俺が!俺達が LOUDSだ!!
LOUDS とは (6/13)● では早速子ノードを求めてみる● 1番のノードに対して
最初の子ノードが 3番であることが分かれば良い( あるノードの子ノードは連続して並んでいて子ノード数はわかっているのでどこまでが子ノードかわかる )
1
43
イ
ドス
(2)
(1) (1)
LOUDS とは (7/13)● まず注目ノード (1番 ) の直前までの子ノード数の合計値を計算する
● 2
(2)
(0)
0 21
7
43 5
8
ア ラ
カ
イ ドス ス
ル
6
20
9
15
10
10
(2) (1) (1)
(1)
(1)
(1)
(1)
(0)(0)
1
43
イ
ドス
LOUDS とは (8/13)● 魔法の数字 1 を足す● 2 + 1 = 3● これで最初の子ノード (3番 ) が求まった!!
(2)
(0)
0 21
7
43 5
8
ア ラ
カ
イ ドス ス
ル
6
20
9
15
10
10
(2) (1) (1)
(1)
(1)
(1)
(1)
(0)(0)
1
43
イ
ドス
LOUDS とは (9/13)
偶然に決まってる!魔法の数字なんて馬鹿馬鹿しい
俺は先に帰るぞ!!!(死亡フラグ的な意味で)
LOUDS とは (10/13)
● ほかの子ノードも求めてみる● 5番のノードに対して
最初の子ノードが 8番であることを求める
5
8 カ
ス (1)
(1)
LOUDS とは (11/13)● まず注目ノード (5番 ) の直前までの子ノード数の合計値を計算する
● 2 + 2 + 1 + 1 + 1 = 7
(2)
(0)
0 21
7
43 5
8
ア ラ
カ
イ ドス ス
ル
6
20
9
15
10
10
(2) (1) (1)
(1)
(1)
(1)
(1)
(0)(0)
5
8 カ
ス (1)
(1)
LOUDS とは (12/13)● 魔法の数字 1 を足す● (2 + 2 + 1 + 1 + 1) + 1 = 7 + 1 = 8● これで最初の子ノード (8番 ) が求まった!!
(2)
(0)
0 21
7
43 5
8
ア ラ
カ
イ ドス ス
ル
6
20
9
15
10
10
(2) (1) (1)
(1)
(1)
(1)
(1)
(0)(0)
5
8 カ
ス (1)
(1)
LOUDS とは (13/13)
● ということは・・・● あるノードまでの子ノード数の合計が定数時間で計算できるなら子ノードの位置も定数時間で計算できる!
● 簡潔データ構造を使えば実現可能!!
● LOUDS ちゃんマジ LOUDS !!!
第 4 章LOUDS を作ってみた
LOUDS を作ってみた (1/7)● 以下のような louds_trie クラスを作った
● 抽象クラス trie を継承● 構築済み basic_trie から louds を構築する
build() を用意
● class louds_trie : public trie {public: void build(const basic_trie &bt); void search(const string &key, vector<pair<string, string>> &value); bool read(const char *filename); void write(const char *filename);};
LOUDS を作ってみた (2/7)
● 具体的なデータ構造について考える
● LOUDS は TRIE の子ノード数を並べたデータ構造● n番目のデータまでの累計値 ( 子ノード数の合計 )
を定数時間で計算できる簡潔データ構造が必要
● 元論文では Unary符号を使っていた● が! Unary符号は性能がよろしくない
LOUDS を作ってみた (3/7)
● そこで VerticalCode(岡野原 , 2005) を使った
● 実装が簡単● デルタ符号並の性能● 以前実装したのが流用できる (個人的な事情 )● 最近公開された dag_vector を使ってもよいかも
● これによってLOUDS(Level-Order Unary Degree Sequence) はLOVES(Level-Order VErtical code Sequence) に生まれ変わった!(※思いつきで名付けた)
LOUDS を作ってみた (4/7)● ちなみに VerticalCode のクラスは以下のようになっている● push(d) で値 d を持つデータを追加● diff(pos) で pos の値を取得● get(pos) で pos までの値の累計値を取得
● typedef unsigned long long ullong;● class vertical_code {
public: void push(ullong d); ullong diff(ullong pos); ullong get(ullong pos);};
LOUDS を作ってみた (5/7)
● 結果、以下のようなデータ構造になった● 子ノード数の列 (LOUDS) を VerticalCode で管理● ノードのラベルは node クラスの vector で管理
● class louds_trie : public trie { vertical_code vc_; vector<node> nodes_;};
LOUDS を作ってみた (6/7)● あとは trie クラスの共通機能
search() を実装する● 具体的には現在のノードから子ノードを探す部分だけいじればOK
● i = g_[pos].a_.begin();e = g_[pos].a_.end();while (i != e) { ......; i++; }
● i = vc_->get(pos – 1) + 1;e = i + vc->diff(pos);while (i != e) { ......; i++; }
louds_trie
basic_trie
LOUDS を作ってみた (7/7)● 無事 louds_trie ができたので
basic_trie との違いを比較してみた
● 辞書データ・クエリ● 住所リスト (117957件 / 3.9MB)
● ファイルサイズ● basic_trie (6.1MB) / louds_trie (3.1MB)
● 使用メモリ● basic_trie (24.9MB) / louds_trie (14.3MB)
● 実行時間 ( 前方一致検索 )● basic_trie (7.3sec) / louds_trie (7.5sec)
まとめ (1/1)
● TRIE とは何か● TRIE は前方一致可能な KVS として使える● TRIE はメモリをたくさん使う
● 省メモリなアルゴリズム● Patricia, Double Array, LOUDS, XBW
● 既存の優れたライブラリ● darts, tx-trie, marisa-trie
● TRIE ライブラリを作ってみた● erika-trie● 普通の TRIE & LOUDS
参考資料 (1/2)
● ライブラリ● darts (http://chasen.org/~taku/software/darts/)● tx-trie (http://code.google.com/p/tx-trie/)● marisa-trie (http://code.google.com/p/marisa-trie/)
● Web サイト● やた @ はてな日記 (http://d.hatena.ne.jp/s-yata/)● Something in C/C++/C# (http://nanika.osonae.com/)● Preferred Research
(http://research.preferred.jp/2011/05/trie_survey/)
参考資料 (2/2)
● 論文● Trie memory (E. Fredkin, 1960)● PATRICIA―Practical Algorithm To Retrieve
Information Coded in Alphanumeric(D. R. Mollison, 1968)
● An efficient digital search algorithm by using a double-array structure (Aoe, J.-I. 1989)
● Space-efficient Static Trees and Graphs(G. Jacobson, 1989)
● Structuring labeld trees for optimal succinctness, and beyond (P. Ferragina, and et. al. , 2005)