Upload
toshiyuki-maezawa
View
3.276
Download
2
Embed Size (px)
Citation preview
ウェーブレット行列 最速攻略 〜予告編〜
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
ウェーブレット行列 最速攻略
echizen_tm Sep. 30, 2012
SPIRE2012より、 ずっとはやい!
アジェンダ � 自己紹介 (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)
� まずは以下の配列を考える
� このデータ列を2進表記すると以下のようになる
4 7 6 5 3 2 1 0 1 4 1 7
1
0
0
1
1
1
1
1
0
1
0
1
0
1
1
0
1
0
0
0
1
0
0
0
0
0
1
1
0
0
0
0
1
1
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
0
1
1
1
1
1
0
0
1
0
1
0
0
0
0
0
0
1
0
0
0
1
1
1 1 0 0 0 0
3 2 1 0 1 1
ウェーブレット行列の構築(6/11)
� 2番目のビット列(赤)の後半に 1番目のビット列(オレンジ)で値が1だったものを置く
4 7 6 5 3 2 1 0 1 4 1 7
1
0
1
1
1
1
1
0
0
1
0
1
0
0
0
0
0
0
1
0
0
0
1
1
1 1 0 0 0 0
3 2 1 0 1 1 4 7 6 5 4 7
0 1 1 0 0 1
ウェーブレット行列の構築(7/11)
� この時点で前半に[0, 3]のデータが 後半に[4, 7]のデータが集まった
1 1 0 0 0 0
3 2 1 0 1 1 4 7 6 5 4 7
0 1 1 0 0 1
0,1,2,3 4,5,6,7
ウェーブレット行列の構築(8/11)
� 3番目のビット列(青)の前半に 2番目のビット列(赤)で値が0だったものを置く
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1
1 0 1 1 4 5
1 0 1 0 1 1 0 1 0 1 0 1
3 2 1 0 1 1 4 7 6 5 4 7
0
4
ウェーブレット行列の構築(9/11)
� 3番目のビット列(青)の後半に 2番目のビット列(赤)で値が1だったものを置く
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1
1 0 1 1 4 5
1 0 1 0 1 1 0 1 0 1 0 1
3 2 1 0 1 1 4 7 6 5 4 7
0
4
1 0 1 0 1
3 2 7 6 7
ウェーブレット行列の構築(10/11)
� 今度はデータが [0,1][2,3][4,5][6,7]の4つの範囲に分割された
1 0 1 1 0 1
1 0 1 1 4 5
0
4
1 0 1 0 1
3 2 7 6 7
0,1 4,5 2,3 6,7
ウェーブレット行列の構築(11/11)
� まとめるとこうなる
1 0 1 1 0 1
1 0 1 1 4 5
0
4
1 0 1 0 1
3 2 7 6 7
4 7 6 5 3 2 1 0 1 4 1 7
1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0
3 2 1 0 1 1 4 7 6 5 4 7
0 1 1 0 0 1
ウェーブレット行列のaccess(1/6)
� access(0)の場合
� 1本目のビット列(オレンジ)の0番目は1なので 2本目のビット列(赤)の後半のどこかにある
� rank(1, 0) = 0 なので 2本目後半の0番目(先頭から6番目)に移動しているはず
1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
ウェーブレット行列のaccess(2/6)
� access(0)の場合
� 2本目のビット列(赤)の6番目は0なので 3本目のビット列(青)の前半のどこかにある
� rank(0, 6) = 4 なので 3本目前半の4番目に移動しているはず
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(3/6)
� access(0)の場合
� これらを繋ぎ合わせると100(2進数)となる
� 10進数で4なのでaccess(0) = 4 が得られた
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1 0 1 0 1 0 1
1 1 1 1 0 0 0 0 0 1 0 1
ウェーブレット行列のaccess(4/6)
� access(5)の場合
� 1本目のビット列(オレンジ)の5番目は0なので 2本目のビット列(赤)の前半のどこかにある
� rank(0, 5) = 1 なので 2本目前半の1番目に移動しているはず
1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
ウェーブレット行列のaccess(5/6)
� access(5)の場合
� 2本目のビット列(赤)の1番目は1なので 3本目のビット列(青)の後半のどこかにある
� rank(1, 1) = 1 なので 3本目後半の1番目(先頭から8番目)に移動しているはず
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(6/6)
� access(5)の場合
� これらを繋ぎ合わせると010(2進数)となる
� 10進数で2なのでaccess(5) = 2 が得られた
1 1 0 0 0 0 0 1 1 0 0 1
1 0 1 1 0 1 0 1 0 1 0 1
1 1 1 1 0 0 0 0 0 1 0 1
ウェーブレット行列のrank(1/9) � rank(4, 10)の場合
� つまり以下の紫の枠にいくつ4があるかを計算したい
4 7 6 5 3 2 1 0 1 4 1 7
0 10
ウェーブレット行列のrank(2/9) � rank(4, 10)の場合
� なので1本目のビット列(オレンジ)では 1となっている部分に注目すればよい
1 1 1 1 0 0 0 0 0 1 0 1
4 1 0 0
0 10
4 7 6 5 3 2 1 0 1 4 1 7
ウェーブレット行列のrank(3/9) � rank(4, 10)の場合
� 1本目のビット列(オレンジ)で1となる数は 2本目のビット列(赤)で後半に移動している
� 1本目の枠の先頭の0は 2本目ではそのまま後半の先頭(6番目)に移動する
1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
0 10
6
ウェーブレット行列のrank(4/9) � rank(4, 10)の場合
� 1本目のビット列(オレンジ)で1となる数は 2本目のビット列(赤)で後半に移動している
� 1本目の枠の終端の10は rank(1, 10) = 5 なので 2本目では後半の5番目(先頭から11番目)に移動する
1 1 1 1 0 0 0 0 0 1 0 1
1 1 0 0 0 0 0 1 1 0 0 1
0 10
6 11
ウェーブレット行列のrank(5/9) � rank(4, 10)の場合
� なので2本目のビット列(赤)では 0となっている部分に注目すればよい
4 1 0 0
1 1 0 0 0 0 0 1 1 0 0 1
11
3 2 1 0 1 1 4 7 6 5 4 7
6
ウェーブレット行列のrank(6/9) � rank(4, 10)の場合
� 2本目のビット列(赤)で0となる数は 3本目のビット列(青)で前半に移動している
� 2本目の枠の先頭の6は rank(0, 6) = 4 なので 3本目では前半の4番目に移動する
1 1 0 0 0 0 0 1 1 0 0 1
6 11
1 0 1 1 0 1 0 1 0 1 0 1
4
ウェーブレット行列のrank(7/9) � rank(4, 10)の場合
� 2本目のビット列(赤)で0となる数は 3本目のビット列(青)で前半に移動している
� 2本目の枠の終端の11は rank(0, 11) = 7 なので 3本目では前半の7番目に移動する
1 1 0 0 0 0 0 1 1 0 0 1
6 11
1 0 1 1 0 1 0 1 0 1 0 1
4 7
ウェーブレット行列のrank(8/9) � rank(4, 10)の場合
� なので3本目のビット列(青)では 0となっている部分に注目すればよい
4 1 0 0
1 0 1 1 0 1 0 1 0 1 0 1
4
1 0 1 1 4 5 4 3 2 7 6 7
7
ウェーブレット行列のrank(9/9) � rank(4, 10)の場合
� 3本目のビット列(青)の枠で囲まれている範囲では 元の配列で4となる数が0に、5となる数が1となっている
� rank(0, 4) = 1 なので枠の先頭までに0が1つある rank(0, 7) = 3 なので枠の終端までに0が3つある
� 3 − 1 = 2 なので枠の中に4は2つあるとわかる
1 0 1 1 0 1 0 1 0 1 0 1
4
1 0 1 1 4 5 4 3 2 7 6 7
7
元のデータに対して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’)を持っておけば良い
1 0 1 1 4 5 4 3 2 7 6 7
0,1 4,5 2,3 6,7
rank(0, i) rank(1, i)
rank(4, i) rank(5, i)
rank(2, i) rank(3, i)
rank(6, i) rank(7, i)
【個人研究】ウェーブレット行列の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%)