Try for Trie

58
TRIE にトライ! ~今日からはじめる TRIE 入門~ echizen_tm July 23, 2011

Transcript of Try for Trie

Page 1: Try for Trie

TRIE にトライ!~今日からはじめる TRIE 入門~

echizen_tmJuly 23, 2011

Page 2: Try for Trie

目次

● 自己紹介 (1 slides)● 第 1 章  TRIE とは (3 slides)● 第 2 章  TRIE を作ってみた (25 slides)● 第 3 章  LOUDS とは (13 slides)● 第 4 章  LOUDS を作ってみた (7 slides)● まとめ (1 slides)● 参考資料 (2 slides)

Page 3: Try for Trie

自己紹介 (1/1)

● ID : echizen_tm● ブログ: EchizenBlog-Zwei

● 職業: web エンジニア● 興味:簡潔データ構造、機械学習● お仕事:コンテンツマッチ、レコメンデーション、

スペルコレクション

● 最近はまっていること: Project Euler

Page 4: Try for Trie

第 1 章TRIE とは

Page 5: Try for Trie

TRIE とは (1/3)

● Edward Fredkin が提案 (1960)● 木構造の一種

● どんなことができるの?● キーワードに対する値を返す (Key-Value Store)● キーワードに前方一致する別のキーワードを獲得する

– 言語 → 言語処理、言語学、言語資源– ロウ → ロウ材、ロウレイズティー、ロウきゅーぶ!

● ライブラリとかあるの?● darts(@taku910 さん ) 、 tx-trie(@hillbig さん ) 、

marisa-trie(@s5yata さん ) など

Page 6: Try for Trie

TRIE とは (2/3)

● TRIE の良いところ● KVS よりリッチな操作ができる● 検索が高速(辞書サイズに対して O(1) )

● TRIE の悪いところ● メモリをたくさん使う

( 素直に実装すると辞書サイズに対して O(NlogN))

実用的なライブラリではデータサイズを小さくするアルゴリズムが使われている

Page 7: Try for Trie

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 を木構造に拡張

Page 8: Try for Trie

第 2 章TRIE を作ってみた

Page 9: Try for Trie

TRIE を作ってみた (1/25)

● 理解を深めるために TRIE を作ってみた

● その名も erika-trie !

● ちなみに前回はCompressed Suffix Array ライブラリ tsubomiを作った

Page 10: Try for Trie

TRIE を作ってみた (2/25)● ちなみに他のライブラリと並べてみると

● そんなに違和感がない! ・・・気がする

darts tx-trie

marisa-trie erika-trie

Page 11: Try for Trie

TRIE を作ってみた (3/25)

● 気をとりなおして実装の解説

● まずは実装の方針を● C/C++で作る● まずはシンプルに● 実用性はとりあえず後回し● 気になった部分に適宜、手を入れていく

● とにかく勉強のために作る!

Page 12: Try for Trie

TRIE を作ってみた (4/25)

まずは必要な機能を考える

Page 13: Try for Trie

TRIE を作ってみた (5/25)● TRIE に必要な機能は以下のとおり● TRIE を構築する

● 用意した辞書(キーワードと値のペアのリスト)からTRIE 木を作る

● アルゴリズムによって適切な構築法が異なる

● TRIE で検索する● 入力キーワードに一致(もしくは前方一致)する

キーワードを TRIE 木から探す● キーワードと値のペアを返す

● TRIE をファイルに書き出す● TRIE をファイルから読み込む

Page 14: Try for 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;};

Page 15: Try for Trie

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);};

Page 16: Try for Trie

TRIE を作ってみた (8/25)

次にデータ構造について考える

Page 17: Try for Trie

TRIE を作ってみた (9/25)● 根から葉までたどるとキーワードができる● 葉はキーワードに対する値を持っている

ドス ス

ル20

15 10

Page 18: Try for Trie

TRIE を作ってみた (10/25)● ノードのラベルについて考える

● 固定長 ( ラベル = 1文字 )● 子ノードのサイズが固定 ( 文字の種類数 )

→固定サイズの配列を作れば定数時間でアクセス可

● 可変長 ( ラベル =任意文字列 )● Patricia 木に拡張しやすい● 子ノードのサイズが不定

→線形探索or辞書順ソートして二分探索でアクセス可

● 拡張性を考えて可変長にした● ラベルは UTF8の 1 文字。子ノードは線形探索

Page 19: Try for Trie

TRIE を作ってみた (11/25)

● ノードは以下のクラスで表現した● ノードのラベルは string型● ノードが葉かどうかは bool型のフラグで管理

● class node {public: string label_; bool is_value_;

node(const string &, bool); ~node();};

Page 20: Try for Trie

TRIE を作ってみた (12/25)● ノードと子ノードのリストを

まとめて element クラスで表現した● 子ノードは int型の elementID で管理● 複数子ノードがあるので vector型で持つ● element の vector で TRIE全体を表す

(↑の添字が element の ID )

● class element {public: node n_; vector<int> a_;

element(const node &); ~element();};

Page 21: Try for Trie

TRIE を作ってみた (13/25)● 変数の部分だけまとめると以下のようになる

● class basic_trie { vector<element> g_;};

● class element { node n_; vector<int> a_;};

● class node { string label_; bool is_value_;};

Page 22: Try for Trie

TRIE を作ってみた (14/25)

データ構造が決まったのでTRIE の構築を考える

Page 23: Try for Trie

TRIE を作ってみた (15/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する

20

アイス: 20アイス: 20

Page 24: Try for Trie

TRIE を作ってみた (16/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する

ス ス

20

10

アラスカ: 10アラスカ: 10

Page 25: Try for Trie

TRIE を作ってみた (17/25)● 素直にデータを 1件ずつ足しこんでいく● 木をたどってノードがなかったら追加する

ドス ス

ル20

15 10

アイドル: 15アイドル: 15

Page 26: Try for Trie

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 文字ずつチェック

子ノードを線形探索

見つからなかったらノード追加

Page 27: Try for Trie

TRIE を作ってみた (19/25)

TRIE を構築したので検索について考える

Page 28: Try for Trie

TRIE を作ってみた (20/25)● まずは単純に入力キーワードを探索● 見つからなかったらそこで終了

ドス ス

ル20

15 10

アイアイ

Page 29: Try for Trie

TRIE を作ってみた (21/25)● 入力キーワードを見つけた場所のノードに対して

子ノードを DFS(縦型探索 ) してキーと値を回収

ドス ス

ル20

15 10

アイアイ

アイス: 20アイス: 20アイドル: 15アイドル: 15

Page 30: Try for Trie

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 文字ずつチェック

子ノードを線形探索

見つからなかったらそこで終わり

見つかったら縦型探索して値を回収

Page 31: Try for Trie

TRIE を作ってみた (23/25)

● 探索について

● DFS(縦型探索、深さ優先探索 )● スタックで実装● 関数の再帰呼び出しで代用可● 木の階層が浅い場合に有効

● BFS(横型探索、幅優先探索 )● キューで実装● 木のノード数が少ない場合に有効

● TRIE は木が浅くてノードが多いので DFS を使った

Page 32: Try for Trie

TRIE を作ってみた (24/25)● ちなみに・・・● 探索アルゴリズムを学ぶにはアリ本

(プログラミングコンテスト チャレンジブック )が超オススメ!● コードが豊富に掲載されている (C/C++ 。 STL多め )● 状況に応じたアルゴリズムの使い方が学べる● 数論に興味が出てくる

● 持ってない人はいますぐ購入しよう!

Page 33: Try for Trie

TRIE を作ってみた (25/25)

ここまでで普通の TRIE が完成!

次はLOUDS に拡張するよ!!

Page 34: Try for Trie

第 3 章LOUDS とは

Page 35: Try for Trie

LOUDS とは (1/13)

● G. Jacobson が提案 (1989)● Level-Order Unary Degree Sequence の略

● 構築済み TRIE から LOUDS を構築する● 作業領域が O(NlogN) から O(N) に

● 具体的には子ノードの ID リストではなく子ノードの数だけ持っていれば良くなった

● 簡潔データ構造によって子ノードの ID が定数時間で計算可能

● ただしノードの追加、削除は不可

Page 36: Try for Trie

LOUDS とは (2/13)

では具体的にLOUDS 構築の手順を見てみよう!

Page 37: Try for Trie

LOUDS とは (3/13)● ノードに幅優先探索の順番 (Level-Order)

で番号をつける

0

21

7

43 5

8

ドス ス

ル6 20

9 15 10 10

Page 38: Try for Trie

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)

Page 39: Try for Trie

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だ!!

Page 40: Try for Trie

LOUDS とは (6/13)● では早速子ノードを求めてみる● 1番のノードに対して

最初の子ノードが 3番であることが分かれば良い( あるノードの子ノードは連続して並んでいて子ノード数はわかっているのでどこまでが子ノードかわかる )

1

43

ドス

(2)

(1) (1)

Page 41: Try for Trie

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

ドス

Page 42: Try for Trie

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

ドス

Page 43: Try for Trie

LOUDS とは (9/13)

偶然に決まってる!魔法の数字なんて馬鹿馬鹿しい

俺は先に帰るぞ!!!(死亡フラグ的な意味で)

Page 44: Try for Trie

LOUDS とは (10/13)

● ほかの子ノードも求めてみる● 5番のノードに対して

最初の子ノードが 8番であることを求める

5

8 カ

ス (1)

(1)

Page 45: Try for Trie

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)

Page 46: Try for Trie

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)

Page 47: Try for Trie

LOUDS とは (13/13)

● ということは・・・● あるノードまでの子ノード数の合計が定数時間で計算できるなら子ノードの位置も定数時間で計算できる!

● 簡潔データ構造を使えば実現可能!!

● LOUDS ちゃんマジ LOUDS !!!

Page 48: Try for Trie

第 4 章LOUDS を作ってみた

Page 49: Try for Trie

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);};

Page 50: Try for Trie

LOUDS を作ってみた (2/7)

● 具体的なデータ構造について考える

● LOUDS は TRIE の子ノード数を並べたデータ構造● n番目のデータまでの累計値 ( 子ノード数の合計 )

を定数時間で計算できる簡潔データ構造が必要

● 元論文では Unary符号を使っていた● が! Unary符号は性能がよろしくない

Page 51: Try for Trie

LOUDS を作ってみた (3/7)

● そこで VerticalCode(岡野原 , 2005) を使った

● 実装が簡単● デルタ符号並の性能● 以前実装したのが流用できる (個人的な事情 )● 最近公開された dag_vector を使ってもよいかも

● これによってLOUDS(Level-Order Unary Degree Sequence) はLOVES(Level-Order VErtical code Sequence) に生まれ変わった!(※思いつきで名付けた)

Page 52: Try for Trie

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);};

Page 53: Try for Trie

LOUDS を作ってみた (5/7)

● 結果、以下のようなデータ構造になった● 子ノード数の列 (LOUDS) を VerticalCode で管理● ノードのラベルは node クラスの vector で管理

● class louds_trie : public trie { vertical_code vc_; vector<node> nodes_;};

Page 54: Try for Trie

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

Page 55: Try for 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)

Page 56: Try for Trie

まとめ (1/1)

● TRIE とは何か● TRIE は前方一致可能な KVS として使える● TRIE はメモリをたくさん使う

● 省メモリなアルゴリズム● Patricia, Double Array, LOUDS, XBW

● 既存の優れたライブラリ● darts, tx-trie, marisa-trie

● TRIE ライブラリを作ってみた● erika-trie● 普通の TRIE & LOUDS

Page 57: Try for Trie

参考資料 (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/)

Page 58: Try for Trie

参考資料 (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)