trie treeならびにdouble array trie treeを用いたパフォーマンスの検証

17
Trie Tree ならびに Double Array Trie Tree を用いた文字列 処理パフォーマンスの検証 1

Upload: moai-kids

Post on 20-Jul-2015

6.542 views

Category:

Technology


2 download

TRANSCRIPT

Page 1: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

Trie Treeならびに Double Array Trie Tree を用いた文字列処理パフォーマンスの検証

1

Page 2: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

1 はじめにブログなどの大量のテキストデータが蓄積される CGMサイトでは、必然的にシステムやプログラムの中でも文字列処理の回数が多くなる。そのため効率的なアルゴリズムの採用は、サイトのプログラム処理速度、ひいては対ユーザのページレスポンスタイムに関わる重要な事案となる。

今回は、文字列の全文一致検索や前方一致検索処理において効率的なアルゴリズムとして知られる「トライ木」についてそのパフォーマンスの検証を行った。

2 TrieTreeについて2.1 アルゴリズムの内容

以下、トライ木についての解説をwikipediaより引用する。トライ木(Trie)とは、順序付き木構造 (データ構造)の一種。プレフィックス木(Prefix Tree)とも呼ばれる。キーが文字列である連想配列の実装構造として使われる。2分探索木と異なり、各ノードに個々のキーが格納されるのではなく、木構造上のノードの位置とキーが対応している。あるノードの配下の全ノードは、自身に対応する文字列に共通するプレフィックス(接頭部)があり、ルート(根)には空の文字列が対応している。値は一般に全ノードに対応して存在するわけではなく、末端ノードや一部の中間ノードだけがキーに対応した値を格納している。[1]

文字列を、先頭文字から一文字ずつを節とする木構造として扱うのがトライ木、といえる。

2.2 一般的に言われるメリット一般的に、以下がトライ木のメリットと言われている。

キー検索が高速 文字列を格納する際にメモリを節約できる。 前方一致文字の検索ならびに完全一致文字の検索に非常に適している。 木構造としてバランスが取れてなくてもよい(ルートノードの下のノードの深さ、数がバラバラでも性能に影響が少ない)

2.3 得意な処理上記のメリットから、ある文章から完全一致する語を抽出する処理や、Common Prefix Search(ある語の集団から、検索キーと前方一致する語をすべて抽出するアルゴリズム )を実装するのに適している。また、この特性から、多くの辞書アプリやスペルチェッカーなどにトライ木のアルゴリズムは採用されている。

以下、Java 標準APIとの比較により確認する。

3 比較(Trie Tree × Java 標準 API)以下のデータを元に、Trie Treeと Java標準 API(String#indexOf, String#startsWith)とのパフォーマンスの比較を行った。なお記事データはファイルから1行ずつ逐次読み込み、辞書データはメモリ上にすべて保管した上で計測を行った。測定結果は、同試験を 10回試行し、最大と最小の値を除いた平均値を算出した。

2

Page 3: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

記事データ 1,000記事 10,000記事 100,000記事

辞書データ 100語 1,000語 10,000語

検証マシンのスペック Macbook Air CPU:1.6GHz Core 2 Duo Memory:2GB

3.1 処理速度3.1.1 完全一致記事データから、辞書に含まれている単語と完全一致する語を抽出する。Java標準APIでは String#indexOfを使用し検証を行った。

○ 1,000記事TrieTree Java API

100語 77.125 142.6251000語 60.25 487.510000語 108.125 5440.25

1000記事

0

1000

2000

3000

4000

5000

6000

1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

3

Page 4: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

○ 10,000記事TrieTree Java API

100語 529.75 600.3751000語 690.375 501810000語 762.75 84526.13

10000記事

0

10000

20000

30000

40000

50000

60000

70000

80000

90000

10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

○ 100,000記事TrieTree Java API

100語 4680.5 7877.251000語 6824.625 69801.6310000語 8438.25 766336

4

Page 5: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

100000記事

0

100000

200000

300000

400000

500000

600000

700000

800000

900000

100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

全般的に TrieTreeの方が処理速度が速いことが分かる。記事数が増えるごとのパフォーマンスの劣化の度合いは TrieTreeは「log(n)」であるのに対し、Java APIを使用したものはほぼ「n」となっている。こちらはそれぞれのアルゴリズムの特性と一致しているため、本計測結果も妥当な結果が得られていると思われる。

3.1.2 .Common Prefix Search記事データから、辞書に含まれている単語のCommon Prefix Searchの結果を抽出する。Java標準APIでは、String#startsWithを使用して検証を行った。○ 1,000記事

Trie Tree Java API100語 14.625 21.6251000語 15.625 56.87510000語 12.625 888.5

5

Page 6: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

1000記事

0

100

200

300

400

500

600

700

800

900

1000

1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

○ 10,000記事Trie Tree Java API

100語 117.25 1621000語 130.375 500.7510000語 116 7986.75

10000記事

0

1000

2000

3000

4000

5000

6000

7000

8000

9000

10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

6

Page 7: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

○ 100,000記事Trie Tree Java API

100語 2308.5 1649.751000語 2129.25 5022.87510000語 2125.375 109272

100000記事

0

20000

40000

60000

80000

100000

120000

100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語

ms Tr ie Tre e

J a va APInlog(n)

7

Page 8: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

こちらについても、全般的に Trie Treeの方が処理が速いことが分かる。完全一致の際は Trie Treeのパフォーマンス劣化度合いは「log(n)」であったのに対し、Common Prefix Searchについては辞書語数によるパフォーマンスの差がほとんど現れない。先頭文字に一致する語を辞書から走査するアルゴリズムのため、ノード探索数の増加が辞書の語数にそれほど比例しないことが理由と考えられる。

3.2 メモリ効率10,000語の辞書データを用いて、Trie Treeと Java標準APIのメモリ使用効率について調査した。なお、Java標準 API側については、配列 ArrayListに String型のインスタンスを詰める形で検証を行った。メモリ使用サイズの検証は、GCが発生しない環境でそれぞれのインスタンスを生成し、その前後でのメモリ空き領域の差から値を算出した。測定結果は、同試験を 10回試行し、最大と最小の値を除いた平均値を算出した。

Trie Tree Java API3222423

21610642

4

一般的には Trie Treeの方がメモリ効率が良いといわれているが、今回の検証では逆の結果となっている。原因としては、今回の実装では Trie Treeを表現するために複数のインスタンスを組み合わせる形となっており、Java標準 APIに比してメモリ使用のオーバーヘッドが多めの実装となっていることが原因と思われる。

当たり前の話ではあるが、アルゴリズム的に省メモリが謳われていても、実装が富豪プログラミングになっていればメモリ効率が劣化することの証明でもある。Trie Tree実装を省メモリ構造に作り変えての検証は、今回は行わない。

4 Double Array Trie TreeについてTrie木アルゴリズムの改良版で最近有名となっている Double Array Trie Treeについても検証を行った。Double Array Trie Treeは、1989年に J.-I. Aoe氏によって提唱されたアルゴリズムである。[2]近年では、工藤拓氏が開発した形態素解析器の「MeCab」にて辞書データを表現する箇所に採用されているのが有名である。[3]

4.1 アルゴリズムの内容Double Array Trie Treeは、要素に数値をもつ二つの配列を使用することで Trie木を表現するアルゴリズムである。以下に挙げるルールにより実装される。

配列は「BASE」と「CHECK」の二つの整数値をもっている。(この配列を BCとする)

文字を整数にマッピングするテーブル CODEにより、対象文字と BC配列のマッピングをする。(このマッピングテーブルをCODEとする)

節 xにおいて文字 cに対応する枝が存在し、その枝をたどることで yに辿りつくことが出来る時 BC[x].BASE + CODE[c] = y BC[y].CHECK = xが成り立つ

文字の終端には

8

Page 9: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

 BASE=(マイナスの値)を設定する。

Trie木をDouble Array Trie Treeの構造に変換する様を図示した例として、以下の図を引用する。上部が Trie木で、下部がDouble Array Trie Treeの構造である。

[4]

4.2 Trie木と比較した一般的に言われるメリット/デメリットについて一般的に以下のように言われている。

4.2.1 メリット 文字列の木構造を数値のみで表現できるため、検索速度が高速。 同様の理由で、木構造データのサイズをコンパクトにできる

4.2.2 デメリット Trie木の構造を配列に変換する処理に時間がかかる。 マッピングテーブルのサイズが大きくなる。 若干構造が複雑なため、実装は煩雑なものになる。特に、ノードの動的追加についてはノード再構築を伴うため実用的なパフォーマンスを出すのは簡単ではないとされている。

5 比較(Trie Tree × Double Array Trie Tree)3.の Trie Tree×Java標準 APIでの比較と同様の条件での比較を、Trie Treeと Double Array Trie Treeにおいても実施した。記事データはファイルから1行ずつ逐次読み込み、辞書データはメモリ上にすべて保管した上で計測を行った。測定結果は、同試験を 10回試行し、最大と最小の値を除いた平均値を算出した。

なお、Trie Treeの計測結果については3.で実施したものをそのまま使用している。

9

Page 10: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

5.1 処理速度5.1.1 完全一致○ 1,000記事

Trie Tree DATT100語 77.125 138.751000語 60.25 140.2510000語 108.125 178.75

1000記事

0

20

40

60

80

100

120

140

160

180

200

1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語

ms

Trie Tre eDATT

○ 10,000記事Trie Tree DATT

100語 529.75 1038.6251000語 690.375 1453.37510000語 762.75 1865.625

10

Page 11: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

10000記事

0

200

400

600

800

1000

1200

1400

1600

1800

2000

10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語

ms

Tr ie Tre eDATT

○ 100,000記事Trie Tree DATT

100語 4680.5 164261000語 6824.625 26766.6310000語 8438.25 61353

100000記事

0

10000

20000

30000

40000

50000

60000

70000

100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語

ms

Trie Tre eDATT

残念ながら期待したような結果は出ず、Java標準 APIを採用した場合にくらべると優秀

11

Page 12: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

なもののDouble Array Trie Treeの方が単純 Trie木に比べてパフォーマンスが劣化している。アルゴリズムを実装する上で Javaのインスタンスをいくつか生成する形になっているため、生成やハンドリングのオーバーヘッドが大きくなっていることが原因と考えられる。また、辞書データが多くなるにしたがって処理の劣化度合いが高まるのは、マッピングテーブルやBC配列の大型化による探索コストの増大が考えられる。後者については、辞書データの分割によりどの程度のパフォーマンスの差が生じるか、後段で別途検証する。

Trieと同様Double Array Trie Treeもパフォーマンスの劣化度合いは基本的に log(n)に殉じている。

5.1.2 Common Prefix Search○ 1,000記事

Trie Tree DATT100語 14.625 28.8751000語 15.625 19.62510000語 12.625 19.75

1000記事

0

5

10

15

20

25

30

35

1000 100記事・ 語 1000 1000記事・ 語 1000 10000記事・ 語

ms

Tr ie Tre eDATT

○ 10,000記事Trie Tree DATT

100語 117.25 193.1251000語 130.375 203.87510000語 116 148

12

Page 13: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

10000記事

0

50

100

150

200

250

10000 100記事・ 語 10000 1000記事・ 語 10000 10000記事・ 語

ms

Trie Tre eDATT

○ 100,000記事Trie Tree DATT

100語 2308.5 1708.6251000語 2129.25 1702.62510000語 2125.375 1626.75

100000記事

0

500

1000

1500

2000

2500

100000 100記事・ 語 100000 1000記事・ 語 100000 10000記事・ 語

ms

Tr ie Tre eDATT

Common Prefix Searchについては、100,000記事での検証にて Trie木をパフォーマンス

13

Page 14: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

で上回っており、Trie木に対する優位性があることが確認できた。Common Prefix Searchでパフォーマンスが発揮できた原因としては、ルートノード直下の節が決まればその配下の節の探索だけで済むため、辞書データが大きくなった結果節の数が増大しても節選択のオーバーヘッドが比較的少ないから、と考えられる。

また完全一致探索のアルゴリズム実装に問題があるとした場合、節探索のオーバーヘッドが大きい実装になっている可能性があり、改善の余地がある。

5.2 メモリ効率10,000語の辞書データを用いて、Trie Tree、Double Array Trie Treeならびに Java標準APIのメモリ使用効率について調査した。Trieならびに Java標準APIについては、測定結果は3.の結果をそのまま採用した。メモリ使用サイズの検証は、GCが発生しない環境でそれぞれのインスタンスを生成し、その前後でのメモリ空き領域の差から値を算出した。測定結果は、同試験を 10回試行し、最大と最小の値を除いた平均値を算出した。

Trie Tree DATT Java API3222423

21207989

616114992

3者の中でDouble Array Trie Treeのメモリ効率がもっとも優秀であることが分かる。一般的にメリットと言われている木構造のコンパクトさが実証された結果となっている。

6 Double Array Trie Treeの問題について6.1 Tree構築にかかる時間

4.2.のデメリットにも記載したとおり、Double Array Trie Treeの既知の問題として Tree構築に時間がかかることが挙げられ、ノードの数や節の数の増大により指数関数的に処理時間が増大する。

こちらを回避するアプローチとしてはいくつか考えられるが、今回はひとつの辞書を複数のDouble Array Trie Treeに分割して構築するアプローチを採用し、どの程度構築時間が短縮できるか、検証を行った。

辞書データ 100語 1,000語 10,000語

分割単位 1つ 16分割

先頭文字の下位1バイトの値によりクラスタリング 28分割

16分割に加え、アスキー文字のみ、ひらがなのみ、カタカナ 10分割(ア行、カ行・・・ワ行)にて分割

結果は以下になる。

14

Page 15: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

分割なし 16分割 28分割100語 365 384 2241000語 5848 2444 207110000語 546164 33802 30544

辞書作成時間比較

0

100000

200000

300000

400000

500000

600000

100語 1000語 10000語

ms

分割なし16分割28分割

分割なしの場合、単語数が 10000を超えた段階で爆発的に処理時間が劣化しているが、分割することにより劣化を和らげることができている。辞書構築の時間短縮には、辞書の分割が効果的であることが実証できた。

6.2 辞書データが多い時の探索時間Double Array Trie Tree の構造的な問題とはいえないかもしれないが、5.の検証によりノード数・節数が増えることによりパフォーマンス劣化が発生していたため、6.1.のアプローチにて辞書分割を行うことでパフォーマンスの改善が行われるかどうか、検証した。

記事データ 100,000記事

辞書データ 100語 1,000語 10,000語

辞書の分割単位 分割なし 16分割 28分割

15

Page 16: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

6.2.1 完全一致分割なし 16分割 28分割

100語 16426 13098.5 24158.51000語 26766.63 17964.38 27041.6310000語 61353 44077.5 71881.88

100000記事

0

10000

20000

30000

40000

50000

60000

70000

80000

100語 1000語 10000語

ms

分割なし16分割28分割

6.2.2 Common Prefix Search分割なし 16分割 28分割

100語 1708.625 1508.25 13791000語 1702.625 1423.125 1354.37510000語 1626.75 1414 1346.125

16

Page 17: Trie TreeならびにDouble Array Trie Treeを用いたパフォーマンスの検証

100000記事

0

200

400

600

800

1000

1200

1400

1600

1800

100語 1000語 10000語

ms

分割なし16分割28分割

完全一致、Common Prefix Searchとも、分割単位を増やすことでパフォーマンスの劣化が緩和されていることが分かる。また、28分割時の完全一致のように、分割数を増やしすぎると逆にパフォーマンスが落ちることも確認できた。Double Array Trie Treeに汎用的に言えることかどうかは確証が持てないが、少なくとも今回実装したロジックにおいては辞書データの適切な分割はパフォーマンスの向上に寄与することが証明できた。

7 考察とまとめ上記の結果をふまえ、文字列走査における Trie木アルゴリズムの有効性が確認できた。また、今回用意した実装には若干の問題がある可能性があるが、アルゴリズムとしてDouble Array Trie Treeのパフォーマンスならびにメモリ効率の優秀性が確認できた。また、Double Array Trie Treeの辞書データ作成や文字列検索のパフォーマンス向上に辞書データの分割が有効であることを確認できた。

今後は、Double Array Trie Treeの Java実装のさらなるブラッシュアップを行っていきたいと考えている。アルゴリズムの改善策として、分岐のない節を一緒くたにまとめ TAILという配列で管理する手法[5]など様々な手法が提唱されているため、それらを比較検証した上で取り入れていきたいと思う。また、Double Array Trie Treeは動的なノード追加の実装難易度が高いことが問題であるが、この点を解消するための実装アルゴリズムや検証なども取り組み甲斐のある課題と思われる。

8 参考文献・URL[1] http://ja.wikipedia.org/wiki/%E3%83%88%E3%83%A9%E3%82%A4%E6%9C%A8[2] http://www2.computer.org/portal/web/csdl/abs/trans/ts/1989/09/e1066abs.htm[3] http://mecab.sourceforge.net/[4] http://nanika.osonae.com/DArray/dary.html のサイトから図を引用[5] http://linux.thai.net/~thep/datrie/datrie.html#Suffix

17