ウェーブレット行列 最速攻略

〜予告編〜
echizen_tm
Sep. 16, 2012

はじめに
—  今回の発表では

ウェーブレット行列(Wavelet Matrix)
というデータ構造の話をします

ウェーブレット行列?
—  ウェーブレット行列は

任意のデータ列に対して
ある範囲内に
特定のデータがいくつあるかを
定数時間で計算することができます

例えばどういうこと?(1)

4 7 6 5 3 2 1 0 1 4 1 7

この範囲に
1がいくつあるか知りたい!

例えばどういうこと?(2)

4 7 6 5 3 2 1 0 1 4 1 7

この範囲に
3以下の数がいくつあるか知りたい!

でも普通に計算すると

4 7 6 5 3 2 1 0 1 4 1 7

範囲の長さに比例した
時間がかかる・・・

もし範囲内に
データが1億件あったら

4 7 6 5 ・・・ 1 0 1 4 1 7

1回の計算で
1億に比例した時間がかかる!

これは困る
—  どんなに長いデータ列に対しても

ある範囲内に
特定のデータがいくつあるかを
同じ時間で計算したい!

それ、
ウェーブレット行列でできるよ
—  ウェーブレット行列は

任意のデータ列に対して
ある範囲内に
特定のデータがいくつあるかを
定数時間(=データ数に依存しない時間)で計算
することができます!(再掲)

つまり
ウェーブレット行列があると
—  2000万のユーザデータの

ID1,000,000からID5,000,000の範囲で
年齢が20歳以上のユーザが何人いるかが
定数時間でわかる

—  5000人の社員データの

社員番号2000から5000の範囲で
年収が500万以上の社員が何人いるかが
定数時間でわかる

—  この中に1人、妹がいる!かどうかが

定数時間でわかる

ウェーブレット行列って
デメリットとかないの?
—  定数時間で計算ってことは
インデックスとか余計なデータが必要なんじゃないの?
→簡潔データ構造を使っているので
データ数に対して充分に小さいインデックスで大丈夫
→簡潔データ構造はデータの動的更新は向いてないので
固定された大きなデータに対して参照が多い場合に有効

—  定数時間っていってもデータ数に依存しないだけで
そんなに速くないんじゃないの?
→ウェーブレット木(既存手法)の2倍高速

ウェーブレット行列の
活用例(1)
—  FM-Index
—  接尾辞配列(Suffix Array)型の検索アルゴリズムでは
— 
— 
— 
— 

最も性能が良いと言われている
データサイズを小さくするために
テキストをBWT(Burrows Wheeler変換)で圧縮している
検索時にRBWT(逆Burrows Wheeler変換)で
必要な部分だけ解凍
RBWTは内部でデータの特定範囲に
クエリ内の文字がいくつ含まれるかを計算している
ウェーブレット行列で大幅に効率化!

ウェーブレット行列の
活用例(2)
—  gwt
—  tb_yasu氏による
大規模グラフ類似度計算ソフトウェア
—  当初はウェーブレット木が利用されていたが
最近ウェーブレット行列に置き換わった
—  詳しくはブログをチェック!

—  tb_yasuの日記

http://d.hatena.ne.jp/tb_yasu/
20120909/1347196146

SPIRE2012より、
ずっとはやい!

ウェーブレット行列
最速攻略
echizen_tm
Sep. 30, 2012

アジェンダ
—  自己紹介 (1 slide)
—  本発表の目的 (1 slide)
—  簡潔データ構造の復習 (9 slides)
—  ウェーブレット行列の構築 (11 slides)
—  ウェーブレット行列のaccess (6 slides)
—  ウェーブレット行列のrank (9 slides)
—  まとめ (1 slide)
—  おまけ(1 slide)
—  参考資料 (2 slides)
—  【個人研究】ウェーブレット行列のrankを2倍高速化する(7 slides)

自己紹介
—  ID: echizen_tm
—  ブログ: EchizenBlog-Zwei
—  職業: webエンジニア
—  興味: 自然言語処理を支える技術
(簡潔データ構造、機械学習)

—  お仕事: レコメンドとか
—  近況: 転職しました

本発表の目的
—  ウェーブレット行列を紹介する
—  2012年10月21日〜25日の

SPIREという国際会議で発表されるデータ構造

—  ウェーブレット行列とは?

→ウェーブレット木と同じ機能を持つデータ構造
(しかもウェーブレット木より簡単で効率的)

—  なんで紹介するの?

→ウェーブレット行列の圧倒的な性能に心奪われたから

—  ウェーブレット木とか知らないんだけど・・・
→ウェーブレット木の知識は不要

簡潔データ構造の復習(1/9)
—  “簡潔データ構造”というデータ構造はない
—  既存のデータ構造(木、配列)に対して簡潔版がある

—  簡潔データ構造は既存のデータ構造を大幅に圧縮する
—  圧縮したまま操作は効率的にできる(解凍しなくてOK)

簡潔データ構造の復習(2/9)
—  主な簡潔データ構造は以下の3つ
—  簡潔ビットベクトル
—  すべての基礎となる簡潔データ構造
—  他の簡潔データ構造を実装するときに必要

—  LOUDS
—  木の簡潔データ構造
—  詳しくは“日本語入力を支える技術”を読みましょう

—  ウェーブレット木
—  配列の簡潔データ構造
—  ウェーブレット行列に取って代わられる予定

簡潔データ構造の復習(3/9)
—  簡潔ビットベクトルは以下の操作を提供する
—  access(i):

i番目の要素を返す

—  rank(b, i):

ビット列のi番目より前にいくつbが出現するかを返す

—  select(b, i):

ビット列でi番目にbが出現する位置を返す
(今回はふれないので忘れましょう)

簡潔データ構造の復習(4/9)
—  access(b, i)の例

—  ビット列0110に対して
—  access(0) = 0, access(1) = 1

access(2) = 1, access(3) = 0

簡潔データ構造の復習(5/9)
—  rank(b, i)の例

—  ビット列0110に対して
—  rank(0, 1) = 1, rank(0, 2) = 1

rank(0, 3) = 1, rank(0, 4) = 2
—  rank(1, 1) = 0, rank(1, 2) = 1
rank(1, 3) = 2, rank(1, 4) = 2

簡潔データ構造の復習(6/9)
—  ウェーブレット木は以下の操作を提供する
—  access(i):

i番目の要素を返す

—  rank(c, i):

配列のi番目より前にいくつcが出現するかを返す

—  select(c, i):

配列でi番目にcが出現する位置を返す
(今回はふれないので忘れましょう)

簡潔データ構造の復習(7/9)
—  access(c, i)の例

—  配列abbcに対して
—  access(0) = a, access(1) = b
access(2) = b, access(3) = c

簡潔データ構造の復習(8/9)
—  rank(c, i)の例

—  配列abbcに対して
—  rank(a, 1) = 1, rank(a, 2) = 1

rank(a, 3) = 1, rank(a, 4) = 1
—  rank(b, 1) = 0, rank(b, 2) = 1
rank(b, 3) = 2, rank(b, 4) = 2
—  rank(c, 1) = 0, rank(c, 2) = 0
rank(c, 3) = 0, rank(c, 4) = 1

簡潔データ構造の復習(9/9)
—  簡潔ビットベクトルの実装については

DSIRNLP#2”作ろう!簡潔ビットベクトル”で解説した
—  以降、ビット列に対しては
access, rankができるものとして扱う

—  ウェーブレット木は

ウェーブレット行列で同じことがより簡単にできる
(のでウェーブレット木の実装方法は気にしなくてOK)

—  復習はここまで!

ウェーブレット行列の構築
(1/11)
—  ウェーブレット行列の目的

—  配列に対するaccessは普通にできる
—  配列に対するrankができるようにしたい
(でもaccessができる状態は維持する)

—  rankとは「ある範囲内」に
「ある数」がいくつあるかを数え上げる操作

ウェーブレット行列の構築
(2/11)
—  ウェーブレット行列の基本的な発想
—  データを2つのグループに分ける操作を繰り返す
—  次第に値が近いデータが近くに寄ってくるようにする
—  同じ値が一箇所に集まったところで個数を数え上げる

ウェーブレット行列の構築
(3/11)
—  まずは以下の配列を考える

4 7 6 5 3 2 1 0 1 4 1 7
—  このデータ列を2進表記すると以下のようになる

1 1 1 1 0 0 0 0 0 1 0 1
0 1 1 0 1 1 0 0 0 0 0 1
0 1 0 1 1 0 1 0 1 0 1 1

ウェーブレット行列の構築
(4/11)
—  最初のビット列(オレンジ)はそのまま
ウェーブレット行列の1行目となる

4 7 6 5 3 2 1 0 1 4 1 7
1 1 1 1 0 0 0 0 0 1 0 1

ウェーブレット行列の構築
(5/11)
—  2番目のビット列(赤)の前半に

1番目のビット列(オレンジ)で値が0だったものを置く

4 7 6 5 3 2 1 0 1 4 1 7
1 1 1 1 0 0 0 0 0 1 0 1
0 1 1 0 1 1 0 0 0 0 0 1
3 2 1 0 1 1
1 1 0 0 0 0

ウェーブレット行列の構築
(6/11)
—  2番目のビット列(赤)の後半に

1番目のビット列(オレンジ)で値が1だったものを置く

4 7 6 5 3 2 1 0 1 4 1 7
1 1 1 1 0 0 0 0 0 1 0 1
0 1 1 0 1 1 0 0 0 0 0 1
3 2 1 0 1 1 4 7 6 5 4 7
1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列の構築
(7/11)
—  この時点で前半に[0, 3]のデータが
後半に[4, 7]のデータが集まった

0,1,2,3

4,5,6,7

3 2 1 0 1 1 4 7 6 5 4 7
1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列の構築
(8/11)
—  3番目のビット列(青)の前半に

2番目のビット列(赤)で値が0だったものを置く

3 2 1 0 1 1 4 7 6 5 4 7
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 0 1 1 0 1 0 1 0 1
1 0 1 1 4 5 4
1 0 1 1 0 1 0

ウェーブレット行列の構築
(9/11)
—  3番目のビット列(青)の後半に

2番目のビット列(赤)で値が1だったものを置く

3 2 1 0 1 1 4 7 6 5 4 7
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 0 1 1 0 1 0 1 0 1
1 0 1 1 4 5 4 3 2 7 6 7
1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の構築
(10/11)
—  今度はデータが

[0,1][2,3][4,5][6,7]の4つの範囲に分割された

0,1

4,5

2,3

6,7

1 0 1 1 4 5 4 3 2 7 6 7
1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の構築
(11/11)
—  まとめるとこうなる

4 7 6 5 3 2 1 0 1 4 1 7
1 1 1 1 0 0 0 0 0 1 0 1
3 2 1 0 1 1 4 7 6 5 4 7
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 4 5 4 3 2 7 6 7
1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の
access(1/6)
—  access(0)の場合

1 1 1 1 0 0 0 0 0 1 0 1
—  1本目のビット列(オレンジ)の0番目は1なので
2本目のビット列(赤)の後半のどこかにある

—  rank(1, 0) = 0 なので

2本目後半の0番目(先頭から6番目)に移動しているはず

1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列の
access(2/6)
—  access(0)の場合

1 1 0 0 0 0 0 1 1 0 0 1
—  2本目のビット列(赤)の6番目は0なので

3本目のビット列(青)の前半のどこかにある

—  rank(0, 6) = 4 なので

3本目前半の4番目に移動しているはず

1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の
access(3/6)
—  access(0)の場合
—  これらを繋ぎ合わせると100(2進数)となる
—  10進数で4なのでaccess(0) = 4 が得られた

1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の
access(4/6)
—  access(5)の場合

1 1 1 1 0 0 0 0 0 1 0 1
—  1本目のビット列(オレンジ)の5番目は0なので
2本目のビット列(赤)の前半のどこかにある

—  rank(0, 5) = 1 なので

2本目前半の1番目に移動しているはず

1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列の
access(5/6)
—  access(5)の場合

1 1 0 0 0 0 0 1 1 0 0 1
—  2本目のビット列(赤)の1番目は1なので

3本目のビット列(青)の後半のどこかにある

—  rank(1, 1) = 1 なので

3本目後半の1番目(先頭から8番目)に移動しているはず

1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列の
access(6/6)
—  access(5)の場合
—  これらを繋ぎ合わせると010(2進数)となる
—  10進数で2なのでaccess(5) = 2 が得られた

1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列のrank(1/9)
—  rank(4, 10)の場合

—  つまり以下の紫の枠にいくつ4があるかを計算したい

10

4 7 6 5 3 2 1 0 1 4 1 7

ウェーブレット行列のrank(2/9)
—  rank(4, 10)の場合

1 0 0

—  なので1本目のビット列(オレンジ)では
1となっている部分に注目すればよい

4 7 6 5 3 2 1 0 1 4 1 7

10

1 1 1 1 0 0 0 0 0 1 0 1

ウェーブレット行列のrank(3/9)

—  rank(4, 10)の場合

10

1 1 1 1 0 0 0 0 0 1 0 1
—  1本目のビット列(オレンジ)で1となる数は

2本目のビット列(赤)で後半に移動している

—  1本目の枠の先頭の0は

2本目ではそのまま後半の先頭(6番目)に移動する

1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列のrank(4/9)

—  rank(4, 10)の場合

10

1 1 1 1 0 0 0 0 0 1 0 1
—  1本目のビット列(オレンジ)で1となる数は

2本目のビット列(赤)で後半に移動している

—  1本目の枠の終端の10は rank(1, 10) = 5 なので

2本目では後半の5番目(先頭から11番目)に移動する

11

1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列のrank(5/9)
—  rank(4, 10)の場合

1 0 0

—  なので2本目のビット列(赤)では

0となっている部分に注目すればよい

3 2 1 0 1 1 4 7 6 5 4 7

11

1 1 0 0 0 0 0 1 1 0 0 1

ウェーブレット行列のrank(6/9)

—  rank(4, 10)の場合

11

1 1 0 0 0 0 0 1 1 0 0 1
—  2本目のビット列(赤)で0となる数は

3本目のビット列(青)で前半に移動している

—  2本目の枠の先頭の6は rank(0, 6) = 4 なので
3本目では前半の4番目に移動する

1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列のrank(7/9)

—  rank(4, 10)の場合

11

1 1 0 0 0 0 0 1 1 0 0 1
—  2本目のビット列(赤)で0となる数は

3本目のビット列(青)で前半に移動している

—  2本目の枠の終端の11は rank(0, 11) = 7 なので
3本目では前半の7番目に移動する

1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列のrank(8/9)
—  rank(4, 10)の場合

1 0 0

—  なので3本目のビット列(青)では

0となっている部分に注目すればよい

1 0 1 1 4 5 4 3 2 7 6 7

1 0 1 1 0 1 0 1 0 1 0 1

ウェーブレット行列のrank(9/9)

—  rank(4, 10)の場合

1 0 1 1 4 5 4 3 2 7 6 7

1 0 1 1 0 1 0 1 0 1 0 1
—  3本目のビット列(青)の枠で囲まれている範囲では

元の配列で4となる数が0に、5となる数が1となっている

—  rank(0, 4) = 1 なので枠の先頭までに0が1つある
rank(0, 7) = 3 なので枠の終端までに0が3つある

—  3 − 1 = 2 なので枠の中に4は2つあるとわかる
元のデータに対してrank(4, 10) = 2 となる

まとめ
—  ウェーブレット行列は

任意の要素を持つ配列に対してaccess, rank, select
の機能を提供する

—  これはウェーブレット木が提供する機能と同等
—  ウェーブレット行列の機能は

ビット列のaccess, rank, selectによって実現される

—  つまり簡潔ビットベクトルの性能が
ウェーブレット行列に大きく影響する

おまけ
—  資料[1] “The Wavelet Matrix”
より転載

—  WT:普通のウェーブレット木
—  WTNP: Levelwiseな

ウェーブレット木の実装

—  WM: ウェーブレット行列

—  RG: R.Gonzalezらによる
簡潔BVの実装

—  RRR: R.Raman,

V.Raman, S.S.Rao
による簡潔BVの実装

参考資料(1/2)
—  [1] “The Wavelet Matrix”

Claude & Navarro; SPIRE2012
http://www.dcc.uchile.cl/~gnavarro/ps/
spire12.4.pdf

—  [2] アスペ日記(takeda25さんのブログ)
http://d.hatena.ne.jp/takeda25/

—  [3] EchizenBlog-Zwei(echizen_tmのブログ)
http://d.hatena.ne.jp/echizen_tm/

参考資料(2/2)
—  ウェーブレット行列のライブラリには以下のものがある
—  libcds: 論文著者(F. Claude)の実装
様々な簡潔データ構造が実装されたすごいライブラリ
https://github.com/fclaude/libcds

—  wavelet-matrix-cpp: takeda25さんの実装
wat-array互換。rank高速化(後述)あり。他にも工夫がたくさん
https://github.com/hiroshi-manabe/wavelet-matrix-cpp

—  shellinford: echizen_tmの実装
FM-Indexライブラリ。rank高速化したウェーブレット行列を含む
https://code.google.com/p/shellinford/

【個人研究】ウェーブレット行列
のrankを2倍高速化する(1/7)
—  ウェーブレット行列の論文”The Wavelet Matrix”
に書かれているrank計算のアルゴリズムは
内部で簡潔ビットベクトルのbv.rankを
2 * log(文字種)回呼び出している
—  文字列の場合2 * log(256) = 16回

—  文字種 * log(データ長)のデータ領域を

追加で持たせることで
簡潔ビットベクトルのbv.rank呼び出し回数を
log(256)回に削減することができる
—  文字列の場合、256*64bit=2KBの追加データ領域で
bv.rank呼び出しを8回に減らすことができる

【個人研究】ウェーブレット行列
のrankを2倍高速化する(2/7)
—  ウェーブレット行列のrankは

データの範囲を絞り込んでいって
最後に2種類のデータしか含まれなくなったら
bv.rankでビットの数を計算するというもの

—  範囲の更新は以下のように行われる(rank(c, i))
—  depth = 0, begin = 0, end = i
—  while (depth < log(文字種)) {

bit = cのdepth番目のbit
begin = bv[depth].rank(bit, begin)
end = bv[depth].rank(bit, end)
depth++;
}

【個人研究】ウェーブレット行列
のrankを2倍高速化する(3/7)
—  範囲の更新は以下のように行われる(rank(c, i))
—  depth = 0, begin = 0, end = i
—  while (depth < log(文字種)) {

}

bit = cのdepth番目のbit
begin = bv[depth].rank(bit, begin)
end = bv[depth].rank(bit, end)
depth++;

—  このアルゴリズムには重要な2つの特徴がある
—  beginとendの更新は互いに独立に行われる
—  beginが最終的に取りうる値は高々”文字種”の数と同じ
(文字列の場合は256)

【個人研究】ウェーブレット行列
のrankを2倍高速化する(4/7)
—  ウェーブレット行列のrankの更新アルゴリズムには
重要な2つの特徴がある
—  beginとendの更新は互いに独立に行われる
—  beginが取りうる値は高々”文字種”の数と同じ
(文字列の場合は256)

—  この2つの特徴から以下のことが言える
—  beginが取りうる値を事前に計算して持っておくことで

bv.rankの呼び出し回数を半分にすることが出来る
—  beginの取りうる値は高々”文字種”の数なので
文字種*log(データ長)の予備領域で済む
—  beginとendは互いに独立なので
beginの途中の値がなくてもendの更新には影響がない

【個人研究】ウェーブレット行列
のrankを2倍高速化する(5/7)
—  改良前のrank(c, i)
—  depth = 0, begin = 0, end = i
—  while (depth < log(文字種)) {

}

bit = cのdepth番目のbit
begin = bv[depth].rank(bit, begin)
end = bv[depth].rank(bit, end)
depth++;

—  改良後のrank(c, i)
—  depth = 0, begin = 事前計算したcに対するbegin, end = i
—  while (depth < log(文字種)) {
}

bit = cのdepth番目のbit
end = bv[depth].rank(bit, end)
depth++;

【個人研究】ウェーブレット行列
のrankを2倍高速化する(6/7)
—  なぜbeginは高々”文字種”の数の値しか取らないのか?
—  ウェーブレット行列の最終行は”2種類の値”ごとのブロック
—  beginは最終的にこのブロックのうちのどれかの先頭に来る
—  それぞれのブロックの先頭位置begin’に対して
bv.rank(0, begin’), bv.rank(1, begin’)を持っておけば良い
rank(0, i)
rank(1, i)

0,1

rank(4, i)
rank(5, i)

4,5

rank(2, i) rank(6, i)
rank(3, i) rank(7, i)

2,3

6,7

1 0 1 1 4 5 4 3 2 7 6 7

【個人研究】ウェーブレット行列
のrankを2倍高速化する(7/7)
—  で、実際性能はどうなの?
—  takeda25さんがwat-arrayとの比較をしてくださっています
—  データ長100,000,000、文字種1,000に対して

ウェーブレット行列(高速版): 1.49 micro sec (53%)
ウェーブレット木(wat-array): 2.51 micro sec (100%)

—  echizen_tm作のFM-Index

(内部でウェーブレット木/行列を使用)での比較
—  データ長4,000,000、文字種256に対して

ウェーブレット行列(高速版): 144 micro sec (67%)
ウェーブレット行列: 190 misco sec (89%)
ウェーブレット木: 214 micro sec (100%)