2016年7月19日

TF-IDFでデータベース内の類似テキストを検索する Part 4 (MADlib svec編)

TF-IDF 感動巨編3部作は前回のエントリで完結したわけですが、今回はその番外編、スピンオフとして「MADlib svec編」をお送りします。

MADlib には、sparse(疎)な配列、つまり多くの要素がゼロであるような配列を扱うデータ型として svec というデータ型があります。
本エントリでは、TF-IDF のベクトルに MADlib の svec を使って、通常の float8[] などとどのように違うのかを見てみます。

■「MADlib」とは何か


MADlib については、ガッツリと割愛します。以前のエントリで詳しくご紹介しましたので、そちらを参照してください。

■「svec」 とは何か


svec は、ゼロの多い sparse な配列を圧縮して保持するデータ型です。データ分析をしていると、頻繁に遭遇するデータの構造になります。

例えば、float8 の配列で以下のようにゼロが並ぶデータがあったとします。
'{0, 33,...40,000個のゼロ..., 12, 22 }'::float8[]
すると、この配列は 320kB 以上のディスク容量またはメモリを消費することになります。ほとんど意味のないゼロを保持するだけのために、これだけのリソースを食ってしまいます。

svec は、この配列を以下のようにランレングス圧縮(RLE圧縮)することでデータサイズを縮小します。
'{1,1,40000,1,1}:{0,33,0,12,22}'::madlib.svec
このように圧縮することによって、5つの整数型と5つの浮動小数点型に集約され、データサイズが劇的に小さくなります。

このようなデータ型を用意することで、ディスク容量とメモリの消費を抑え、大量のデータの処理を可能にします。(もちろん、演算処理時には圧縮されたデータを展開しながら行いますので、そのCPUコストは発生します)

さらなる詳細はマニュアルを参照してください。

■float8[] を svec に変換する


まず、TF-IDF 感動巨編3部作が完結した状態のテーブルから始めます。
この時、以下のようなテーブル定義になっているはずです。
snaga=> \d pgsql_doc
                                  Table "public.pgsql_doc"
  Column   |        Type        |                         Modifiers
-----------+--------------------+-----------------------------------------------------------
 docid     | integer            | not null default nextval('pgsql_doc_docid_seq'::regclass)
 filename  | text               | not null
 html      | text               | not null
 plain     | text               |
 tf        | jsonb              |
 tfidf     | jsonb              |
 tfidf_vec | double precision[] |
Indexes:
    "pgsql_doc_pkey" PRIMARY KEY, btree (docid)

snaga=>
この時、tfidf_vec のカラムは、以下のようにゼロの多い float8 の配列になっています。
snaga=> SELECT substring(tfidf_vec::text from 0 for 100) FROM pgsql_doc LIMIT 1;
                                              substring
-----------------------------------------------------------------------------------------------------
 {0,0,0,0,0,0,0,0.0328645679655572,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.0
(1 row)

snaga=>
この float8[] を svec に変換するには、単に madlib.svec 型へキャストすれば完了です。
snaga=> SELECT substring(tfidf_vec::madlib.svec::text from 0 for 100) FROM pgsql_doc LIMIT 1;
                                              substring
-----------------------------------------------------------------------------------------------------
 {7,1,31,1,4,1,18,1,70,1,88,1,4,1,51,1,11,1,45,1,1,1,52,1,2,1,4,1,1,7,1,1,1,36,1,1,1,36,1,106,1,14,1
(1 row)

snaga=>

■float8[] と svec のテーブルを用意する


まず、比較のために、docid, filename カラムと、float8[] の tfidf_vec カラム、もしくは svec の tfidf_svec カラムだけを保持するテーブル pgsql_doc_vec と pgsql_doc_svec を作成します。

tfidf_svec カラムは madlib.svec 型になっていることを確認します。
snaga=> CREATE TABLE pgsql_doc_vec AS SELECT docid,filename,tfidf_vec FROM pgsql_doc;
SELECT 1304
snaga=> \d pgsql_doc_vec
        Table "public.pgsql_doc_vec"
  Column   |        Type        | Modifiers
-----------+--------------------+-----------
 docid     | integer            |
 filename  | text               |
 tfidf_vec | double precision[] |

snaga=> CREATE TABLE pgsql_doc_svec AS SELECT docid,filename,tfidf_vec::madlib.svec as tfidf_svec FROM pgsql_doc;
SELECT 1304
snaga=> \d pgsql_doc_svec
    Table "public.pgsql_doc_svec"
   Column   |    Type     | Modifiers
------------+-------------+-----------
 docid      | integer     |
 filename   | text        |
 tfidf_svec | madlib.svec |

snaga=>

■float8[] と svec のデータサイズを比較する


まず、2つのテーブルのデータサイズを比較してみます。
snaga=> \d+
                             List of relations
 Schema |        Name         |   Type   | Owner |    Size    | Description
--------+---------------------+----------+-------+------------+-------------
 public | pgsql_doc           | table    | snaga | 407 MB     |
 public | pgsql_doc_docid_seq | sequence | snaga | 8192 bytes |
 public | pgsql_doc_svec      | table    | snaga | 3872 kB    |
 public | pgsql_doc_vec       | table    | snaga | 6752 kB    |
(4 rows)

snaga=>
上記を見て分かる通り、MADlib の svec 型を使ったテーブルの方が 3MB ほど小さくなっています。

このようにデータサイズを小さくすることによって、ディスクサイズの節約、I/O読み込みの抑制、バッファキャッシュの消費抑制などが実現され、ひいてはパフォーマンスの向上につながります。

■float8[] と svec のパフォーマンス比較


それでは、上記で作ったテーブルとカラムを使って、前回用いた「wal.html と類似のドキュメントを検索する」クエリでパフォーマンスを比較してみます。

float8[] 型を使う場合のクエリは以下の通りです。
EXPLAIN ANALYZE SELECT
  filename,
  euclidean_distance(tfidf_vec, (SELECT tfidf_vec FROM pgsql_doc_vec WHERE filename = 'wal.html') )
FROM
  pgsql_doc_vec
ORDER BY
  2;
svec 型を使う場合のクエリは以下の通りです。
EXPLAIN ANALYZE SELECT
  filename,
  euclidean_distance(tfidf_svec::float8[], (SELECT tfidf_svec::float8[] FROM pgsql_doc_svec WHERE filename = 'wal.html') )
FROM
  pgsql_doc_svec
ORDER BY
  2;
上記のクエリを5回ずつ実行した結果、
  • float8[] 使用時:平均 4757.6 ミリ秒
    • 4738.021, 4739.953, 4734.725, 4793.513, 4781.721
  • svec 使用時:平均 4379.5 ミリ秒
    • 4395.491, 4315.103, 4408.664, 4342.631, 4435.416
となり、svec を使用した方が1割ほど速い、という結果になりました。

データが圧縮されたことによって、パフォーマンス改善が実現されているようです。

■まとめ


今回は、MADlib で提供されている svec 型を使ってみました。

今回のケースでは1割程度のパフォーマンス改善になりましたが、当然ながらデータ圧縮の程度によってパフォーマンス向上の度合いは変わってくるでしょうし、データ量によっても変わってくると思われます。

興味のある方は、ぜひトライしてみていただければと思います。

では、また。

0 件のコメント:

コメントを投稿