CreateField Blog

オープンソースを使って個人でWebサービスを開発・運営していたブログ

ビットパラレルを使ってGroongaで高速な編集距離関数の検証

Groongaではサジェスト機能のためにedit_distance関数が実装されています。

これはO(nm)の計算量が必要な動的計画法で文字数が多くなると結構遅くなります。

そこで、高速化するためにビットパラレル法をGroongaの関数で実装してみて比較してみました。

naoa/groonga-edit-distance · GitHub

ビットパラレル法

これのFigure 3.と4.のMyersの方の実装をしてみました。

A Bit-Vector Algorithm for Computing Levenshtein and Damerau Edit Distances Included in the proceedings of the Prague Stringology Conference 2002 (PSC 2002). http://www.sis.uta.fi/~hh56766/pubs/psc02.pdf

ビット演算で並列に演算して高速化します。計算量はO(n/w m)のようです。 これ長い方を左にするとお得ぽいので長い方が左になるように選択しています。 64文字以下だとO(m)です。mは短い方の文字数。

とりあえず64文字までしか対応していません。64を超えると動的計画法が利用されます。

マルチバイト用の関数では文字をハッシュテーブルで整数値にマッピングしています。文字が増えれば増えるほどマッピングのコストがかかります。マッピングのコストを抑えるために1回のコマンド実行中ハッシュテーブルと左側の文字列のマッピング結果はキャッシュしています。

実験結果

100万件のレコードのscorerに実装したdamerau_edit_distance, edit_distance_bp, edit_distance_bp_varを使ってスコアを求めます。結果出力とスコア代入自体のコストもあるため、score代入のみのケースも示します。なお、全部transposition入りです(たいして差はなかったので)。

文字数x文字数 score代入のみ 動的計画法 ビットパラレル固定長 ビットパラレル可変長
62x62 0.31589 sec 74.16554 sec 1.38189 sec 6.19149 sec
3x62 0.31312 sec 5.26055 sec 0.91457 sec 5.87688 sec
62x3 0.31560 sec 5.73797 sec 0.93845 sec 1.81229 sec
15x15 0.31624 sec 5.40047 sec 0.93917 sec 2.79704 sec
3x3 0.31591 sec 1.19619 sec 0.79177 sec 1.37829 sec

ビットパラレルの方は文字が長くなってもあまり遅くなっていません。ビットパラレル可変長は文字をハッシュテーブルでマッピングしているため、文字数が3文字と非常に少ない場合は効果が低くむしろ時間がかかっています。

3x62の可変長は62x3に比べ結構遅くなっています。可変長の場合、左側の文字列とハッシュテーブルをキャッシュ*1しているのですが、右側はしていません。都度右側の長い文字列をマッピングするのに時間がかかっています(100万レコードの比較なので100万×62)。62x3の場合はキャッシュされる左側が長く、右側が3文字のマッピングだからその分速いです。固定長の方はマッピングがいらないので変わりません。

カラムの文字列をあらかじめマッピングした結果の整数ベクターカラムにしておけば、都度マッピングはいらなくなり、マッピングコストはほとんどなくなるかもしれません。ただ、それは事前に作る必要があって使いづらそうです。

感想

英語の64文字までの特に文字が長いケースで大きく速度の向上が期待できそうです。 日本語でも文字数がある程度あれば、向上が期待できますが、ハッシュテーブルにマッピングするのがめんどくさいです。

ただ、この方法では2つの文字列の比較の計算量がO(m)になったとしても100万レコードとの比較の場合、O(m)×100万が必要になっていまいます。

他の高速な編集距離の実装方法として、levenshtein automataというO(max edit distance) versionってのがあるらしく、O(n/w m)よりも大分はやそうですね。あいまい検索用途ならせいぜい1文字や2文字程度しかいらないのでよさそうです。

Levenshtein automata can be simple and fast

Luceneではこれが実装されているみたいです。

http://blog.mikemccandless.com/2011/03/lucenes-fuzzyquery-is-100-times-faster.html

さきにこっちにきづけばよかった。。今度levenshtein automataを実装できそうだったら実装してみたいです。

このpatlの実装参考になるかな。

https://code.google.com/p/patl/source/browse/trunk/uxn/patl/levenshtein.hpp

*1:1回のselectコマンド実行中、すなわち100万レコードの比較中

Groongaでb Bit MinHashを使って高速に類似検索

Groonga Advent Calendar 2015の23日目の記事です。

はじめに

GroongaでJaccard係数を計算するプラグインを作る - CreateField Blog こちらの記事では、GroongaでJaccard係数を計算できるプラグインを作りました。しかしながら、毎回すべてのレコードについて類似度を計算すると非常に計算量が多くなってしまいます。

そこで、b Bit MinHashという方法を使って高速にJaccard係数を推定する方法を実装して試してみました。

naoa/groonga-minhash · GitHub

ハッシュ関数にはMurmur3のc port、乱数の生成にはXorShiftを利用しています。

MinHashとは

2つの集合に対するランダムなハッシュ関数による最小ハッシュ値が一致する確率は、Jaccard係数に等しいという性質を持ちます。

こちらでかなりわかりやすく説明されています。

乱択データ構造の最新事情 −MinHash と HyperLogLog の最近の進歩− - iwiwiの日記

www.slideshare.net

これ、結構おもしろい性質ですね。

1文書の集合の要素数をk個の最小ハッシュ値に削減することができます。kを大きくすればするほど、正しい確率に近づきます。

b Bit MinHashとは

MinHashでは集合の要素をk個の最小ハッシュ値で表現することができますが、さらに容量削減、演算量削減のため、提案されたのがb Bit MinHashです。b Bit MinHashは最小ハッシュ値の下位bビットのみを保存して演算に使うという方法です。当然ハッシュ値を少ない数値に落としますので衝突しまくります。1ビットなら50%の確率で間違っているはずです。

しかし、そのぶんkを大きくして、衝突確率を考慮して補正することにより、そこそこの精度で高速、且つ空間効率が良くJaccard係数が推定できるとして、良く利用されているようです。

b-Bit MinHashによる高速かつ省スペースな類似度判定 | SmartNews 開発者ブログ

1ビットの場合、ビット演算のみで2つの集合の最小ハッシュ値の一致数を計算できるので、非常に高速に計算できそうです。

精度

実際にどのくらいの精度が得られるか簡単に試してみました。

{A,B,C,D,....}{B,C,D,.....}と65次元ぐらいの集合を1つずつずれていくように用意してb=1ビット、k=64のb Bit MinHashでJaccard係数を推定してみます。正しければアルファベット順に類似度が低くなっていくはずです。

    [
      1000,
      [ "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa" ]
    ],
    [
      968,
      [ "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab" ]
    ],
    [
      937,
      [ "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae" ]
    ],
    [
      937,
      [ "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad" ]
    ],
    [
      937,
      [ "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac" ]
    ],
    [
      906,
      [ "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj" ]
    ],
    [
      906,
      [ "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah" ]
    ],
    [
      906,
      [ "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai" ]
    ],
    [
      906,
      [ "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af" ]
    ],
    [
      906,
      [ "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag" ]
    ],
    [
      843,
      [ "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj", "ak" ]
    ],
    [
      843,
      [ "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj", "ak", "al", "am" ]
    ],

類似度は都合により1000倍してます。1000は1です。

B>C>[F,E,D]>[K,I,J,G,H]>[L,N] と概ね正しい順番が得られています。ただ、結構同じ類似度になってしまっていますね。 上のスライドにあるようにb Bit MinHashで高類似度は狭い範囲でスケッチを比較することにより、少し精度を出すのが難しいようです。*1

ただ、kを128とかもっと増やせば多少改善できそうですし、1ビット*kの数値だけ保存しておけば良いので、空間効率が良く類似度の演算も簡単なので結構使えそうですね*2

性能

単純にJaccard係数を求めるのと、b Bit MinHashでJaccard係数を推定する速度を比較してみました。 集合の要素数はすべて10です(k個の最小ハッシュ値しか保存してないので集合の要素数は類似度の計算には関係ありません)。

文書数 Jaccard係数 b Bit MinHash (b=1, k=64) b Bit MinHash (b=1, k=64) ソートなし
1K 0.408026218414307 sec 0.000944852828979492 sec -
10K 3.95593476295471 sec 0.0039210319519043 sec -
100K 27.2713441848755 sec 0.034470796585083 sec -
1M 363.50066947937 sec 0.344359159469604 sec 0.0878760814666748 sec
5M - 1.81920456886292 sec 0.420000314712524 sec

かなりはやいですね。5百万でも2秒かかりません。なお、この2秒は500万件のクイックソートがほとんどの時間を占めています。ソートなしだと0.4秒です。実際は高類似度だけを閾値で切ればいいので、5Mでも0.数秒で上位を計算できそうです。

おわりに

b Bit MinHashを使えば数百万以上の文書数でも高速且つそこそこの精度で類似検索ができることがわかりました。

結構使えそうなので、これを使ってレコメンドとかやってみたいなぁと考えています。

つぶやき

現在コマンドで実装されていますが、スコア関数のほうがselectで使えて使い勝手がいいのかなぁ。対象スケッチをコマンド実行中にキャッシュできればそっちでもいい気がする。

あと重み付きJaccardとかもできないか検討しよう。

上位の部分だけをあとで真のJaccard係数を計算するのもいいかもしれない。

このMinHash結構色々使えそう。たとえば文字Ngramを作って編集距離の前にざっくり文字列の類似度を求めてから編集距離を求めるとあいまい検索がはやくなりそう

*1:高類似度を改善するためにOdd Sketchesという手法が提案されているようです。すこし実装してみたのですが、うまくいかなかったのでまた今度少し検討したいと考えています。

*2:現状64までしか実装してませんが。

GroongaでJaccard係数を計算するプラグインを作る

Groonga Advent Calendar 2015の22日目の記事です。

はじめに

Groongaでのタグ検索と表記揺れとの戦い at Groonga Meatup 2015 - CreateField Blog こちらの発表では、編集距離ベースで誤字脱字ぽい類似タグを抽出して表記揺れを抽出する話をしました。

しかしながら、編集距離ベースでは文字情報しか見ていないので、文字数が少ないものや略語などの類似タグを抽出することはできません。

そこで、他のタグとの関連性で類似度を抽出するためGroongaのカラムでjaccard係数を計算できるプラグインを作りました。

naoa/groonga-minhash · GitHub

Jaccard係数とは

Jaccard係数とは、2つの集合の類似度を測る尺度です。類似ドキュメントやレコメンドなどに利用されます。 Jaccard係数は以下の式で表せられます。

J(A,B) = A∩B / A∪B

Jaccard index - Wikipedia, the free encyclopedia

A∩BはAND集合、すなわちAとBの要素のうち重なっている集合を示します。 A∪BはOR集合、すなわちAとBの要素を足し合わせた集合を示します。

たとえば、

A:{'商品A', '商品B', '商品C'} 
B:{'商品A', '商品C', '商品D'}
A∩B:{'商品A', '商品C'}
A∪B:{'商品A', '商品B', '商品C', '商品D'}

の場合、AとBのJaccard係数はA∩B/A∪B: 2/4 = 0.5となります。

column_similarityコマンド

Groongaのカラムのデータで類似度を計算できるコマンドです。現状Jaccard係数のみ実装されています。

naoa/groonga-minhash · GitHub

たとえば、以下のようにUsersというテーブルにpurchase_historiesというベクターカラムがあるとします。

plugin_register commands/recommend
[[0,0.0,0.0],true]
table_create Users TABLE_HASH_KEY ShortText
[[0,0.0,0.0],true]
column_create Users purchase_histories COLUMN_VECTOR ShortText
[[0,0.0,0.0],true]
load --table Users
[
{
  "_key": "Taro",
 "purchase_histories": ["Product A", "Product B"]
},
{
  "_key": "Jiro",
 "purchase_histories": ["Product A", "Product B", "Product C"]
},
{
  "_key": "Saburo",
 "purchase_histories": ["Product D", "Product E", "Product F"]
},
{
  "_key": "Shiro",
 "purchase_histories": ["Product A", "Product C", "Product F"]
}
]
[[0,0.0,0.0],4]

この場合、Taroというユーザのpurchase_historiesと最も似たpurchase_historiesを持つユーザはJiroであることを抽出することができます。

column_similarity Users purchase_histories \
--key Taro --output_columns _key,_score,purchase_histories
[
  [0,0.0,0.0],
  [
    [3],
    [
      ["_key","ShortText"],
      ["_score","Int32"],
      ["purchase_histories","ShortText"]
    ],
    [
      "Jiro",
      666,
      ["Product A","Product B","Product C"]
    ],
    [
      "Shiro",
      250,
      ["Product A","Product C","Product F"]
    ],
    [
      "Saburo",
      0,
      ["Product D","Product E","Product F"]
    ]
  ]
]

なお、現状_scoreはfloat出力できないため、便宜上1000倍されています。Groonga本体で小数出力ができるようになったら変更する予定です。

上記の例ではカラムは1つでしたが、複数のカラムを指定することも可能です。また、Groongaのスクリプト構文で検索した結果のみのjaccard similarityを計算することもできます。詳細はGitHubを参照してください。

今後について

テーブルの全レコードの類似度を計算するのは計算量がとても大きくなります。そこで、MinHash(b-bit MinHash, Odd Sketchesなど)と呼ばれる手法を使って高速にjaccard_similarityを推測できるコマンドを実装中です。

また、せっかくJaccard係数を実装するので簡単なレコメンド(ユーザベース協調フィルタリング)も試しに実装してみようと思っています。

JavaScriptでクライアントサイドだけで日本語PDF出力する

クライアントサイドでPDF出力できればサーバ負荷軽減できていいなぁとか考えることがあると思います。

そんなときは、 bpampuch/pdfmake · GitHub に日本語フォントを導入することにより 日本語でクライアントサイドだけでPDF出力することができます。

NotoSansを使おうと思ったのですが、otf形式でttfに変換してもうまく動きませんでしたので、以下の源真ゴシックのttfファイルを利用させてもらいました。

jikasei.me

ただ日本語のフォントは非常にサイズが大きく1つの太さの種類で5Mバイト以上あります。

これでは、クライアントサイドにやらして負荷を下げられればいいなという目論見よりも通信負荷の方が問題になってしまいます。

そこで、あまり使われない漢字等を省いてサブセット化して容量を減らします。 こちらを利用させてもらいました。

サブセットフォントメーカー

とりあえずNormalだけですが、2.2Mほどになりました。 もう少し減らせるかもしれません。

pdfmakeでこのフォントを使ってjsに変換します。

git clone https://github.com/bpampuch/pdfmake
cd pdfmake
npm install grunt grunt-text-replace grunt-browserify grunt-contrib-uglify grunt-dump-dir grunt-contrib-concat
mkdir example/font/bk
mv example/font/* example/font/bk
mv *ttf example/font
grunt dump_dir

Running "dump_dir:fonts" (dump_dir) task
File "build/vfs_fonts.js" created.

こちらのフォークではサブセットした源真ゴシックでjsを生成済みです。

naoa/pdfmake · GitHub

pdfmakeと生成されたbuild/vfs_fonts.jsを読みこませれば日本語でpdf出力することができるようになります。

  pdfMake.fonts = {
    GenShin: {
      normal: 'GenShinGothic-Normal-Sub.ttf',
      bold: 'GenShinGothic-Normal-Sub.ttf',
      italics: 'GenShinGothic-Normal-Sub.ttf',
      bolditalics: 'GenShinGothic-Normal-Sub.ttf'
    }
  }
  defaultStyle = 'GenShin'

  docDefinition = {
    content: '日本語テスト',
    defaultStyle: {
      font: defaultStyle
    }
  }
  pdfMake.createPdf(docDefinition).open()
  pdfMake.createPdf(docDefinition).download()

f:id:naoa_y:20151216052121p:plain

ちょっと前に調べたときには日本語できなくて残念だなぁと思ったことがあって、今回また調べたら実現できたので情報を残しておきます。

Groongaからword2vecを使って類似文書を取得してみる

Groonga Advent Calendar 2015の13日目の記事です。

全文検索エンジンGroongaからword2vecを簡単に使えるプラグイン - CreateField Blog こちらで作ったプラグインのsentence_vectorsオプションを試してみました。

naoa/groonga-word2vec · GitHub

  • 学習ファイル生成
dump_to_train_file docs companies_[company:],categories_[category:],title/,body/@$ \
  --filter 'year >= 2010' --sentence_vectors 1

130万文書ぐらい

  • 学習
word2vec_train --min_count 1 --window 20 --cbow 0 --threads 12 --iter 10 --sentence_vectors 1
  • 元の文書
> select docs --filter "_id == 10876407" --limit 1 --offset 0
--n_sort 1 --output_columns app_id,title,company,body --output_pretty yes
[
  [0,1449906004.79365,0.00154924392700195],
  [
    [
      [1],
      [
        ["app_id","ShortText"],
        ["title","LongText"],
        ["company","company"],
        ["body","LongText"]
      ],
      [
        "JP20100000014",
        "画像形成装置、印刷データ生成装置、プログラム、及び印刷データ生成方法",
        [
          "コニカミノルタビジネステクノロジーズ株式会社"
        ],
        "複数の演算部を用いてPDLデータから印刷データを生成する際の処理速度を向上させる。
【解決手段】本発明に係る画像形成装置は、PDLデータを受信する受信部と、複数の演算部を有し、
前記複数の演算部のそれぞれがページ単位で処理を行うタスク又はバンド単位で処理を行うタスクを
実行することにより、前記PDLデータに基づいて印刷データを生成する画像処理部と、前記PDL
データを解析して印刷ページ数を取得し、当該取得された印刷ページ数に応じて、前記ページ単位で
処理を行うタスクと前記バンド単位で処理を行うタスクのそれぞれに割り当てる前記演算部の数を
動的に設定する制御部と、前記生成された印刷データに基づき印刷媒体に画像を形成する印刷部と、
を備える。【選択図】図5A"
      ]
    ]
  ]
]
  • 類似文書
> word2vec_distance "doc_id:10876407" --sentence_vectors 1
 --limit 10 --offset 0  --n_sort 10 
 --table ftext --column app_id,title,company,body --output_pretty yes
[
  [0,1449906234.40363,1.26499772071838],
  [
    [10],
    [
      ["app_id","ShortText"],
      ["title","LongText"],
      ["company","company"],
      ["body","LongText"]
    ],
    [
      "JP20100000012",
      "画像形成装置、印刷データ生成装置、プログラム、及び印刷データ生成方法",
      [
        "コニカミノルタビジネステクノロジーズ株式会社"
      ],
      "(57)【要約】【課題】複数の演算部を用いてPDLデータから印刷データを生成する場合に、
印刷対象となる個々のPDLデータに応じて最適な演算部の割り当てを行うことを可能とする。
【解決手段】本発明に係る画像形成装置は、PDLデータを受信する受信部と、複数の演算部を有し、
前記複数の演算部のそれぞれが前記PDLデータの言語解析処理又は前記言語解析処理後のラスタライズ
処理を実行することにより、前記PDLデータに基づいて印刷データを生成する画像処理部と、前記
PDLデータに基づいて前記ラスタライズ処理の重み値を算出し、当該算出された重み値に応じて、
前記言語解析処理と前記ラスタライズ処理のそれぞれに割り当てる前記演算部の数を動的に設定する
制御部と、前記生成された印刷データに基づき印刷媒体に画像を形成する印刷部と、を備える。
【選択図】図5A"
    ],
    [
      "JP20110129405",
      "画像形成装置、画像形成方法、及びコンピュータプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】 1ページを複数のブロックに分割するための処理の負荷を分散させる。
【解決手段】 PDLデータから中間データを生成する処理と、中間データを画像データに変換する
処理とを、別々のハードウェア(CPU106、描画処理H/W109)で実現する。CPU106は、
複数のバンドに分割された中間データを生成する。描画処理H/W109は、1つのバンドを複数の
ブロックに分割し、各ブロックの画像データを生成する。【選択図】 図1"
    ],
    [
      "JP20100228837",
      "画像形成装置、画像処理方法、プログラム",
      [
        "株式会社リコー"
      ],
      "【要約】【課題】描画部の監視を必要としないで描画部への描画範囲の割り当てを動的に行う
ことができる画像形成装置及び画像処理方法を提供すること。【解決手段】外部から取得した印刷データ
の画像を転写媒体に印刷する画像形成装置150であって、印刷データを解析し、描画対象とされる
オブジェクトの描画命令をページ毎に記述した描画命令データを生成する描画命令生成手段12と、
描画命令データを記憶する描画命令データ記憶手段15と、描画命令データを読み出して、予め分割
されている描画範囲に対応づけられた描画命令を実行して描画処理する複数の描画処理手段302と、
第一の描画範囲の描画処理が終了した第一の描画処理手段から描画終了の通知を受けた第二の描画
処理手段が第二の描画範囲を分割して、第一の描画処理手段に第二の描画範囲の一部の描画を要求する
描画範囲制御手段303を有する。【選択図】図1"
    ],
    [
      "JP20120013853",
      "画像処理装置,画像形成装置及び画像処理方法",
      [
        "セイコーエプソン株式会社"
      ],
      "【要約】【課題】複数種類のフォーマットの画像データを所定形式の変換データに変換
する画像処理装置において、処理を効率的に行う。【解決手段】画像データの印刷要求を入力し、
入力された印刷要求で要求された印刷対象の画像データのフォーマットを判定し、判定された
フォーマットに応じて、変換処理に用いられるリソースを確保する処理でありフォーマット毎に
異なる所定のリソース確保処理としてのフォーマットA用リソース確保処理又はフォーマットB用
リソース確保処理を実行する(ステップS100〜S240)。続いて、判定されたフォーマット
に応じて、確保されたリソースを用いて印刷対象の画像データから変換データを作成する変換処理
を実行する(ステップS250)。そして、判定されたフォーマットに応じたリソースが既に確保
されているときには(ステップS130,S190でYES)、変換処理前のリソース確保処理を
省略する。【選択図】図2"
    ],
    [
      "JP20130191649",
      "画像形成装置、画像形成装置における画像処理方法及びプログラム",
      [
        "株式会社リコー"
      ],
      "【要約】【課題】  プリンタ描画の初期化処理を高速に行うことで、画像形成効率を向上
させる。【解決手段】画像形成装置であって、フレームメモリ211aに記憶したビットマップ
イメージを、当該ビットマップイメージを生成した印刷データに基づき消去する、それぞれクリア
処理部206の第1の消去手段及び前記フレームメモリ211aに記憶したビットマップイメージ
を全面消去する第2の消去手段と、第1または第2の消去手段によりフレームメモリ211aに
記憶されたビットマップイメージの消去に掛かる時間と、次に記憶するビットマップイメージの
生成に掛かる時間を比較する比較手段204aと、前記比較手段204aの比較結果に基づき、
当該生成されたビットマップイメージの消去手段として、第1または第2の消去手段のいずれかを
選択する選択手段204bを有する。【選択図】図2"
    ],
    [
      "JP20130192449",
      "印刷システム、印刷方法および印刷プログラム",
      [
        "コニカミノルタ株式会社"
      ],
      "【要約】【課題】印刷の全体処理時間を短縮できる印刷装置を提供する。【解決手段】
複数の描画用オブジェクトの画像データおよび印刷設定を含む印刷ジョブから、印刷ジョブに
属する各ページにおける前記描画用オブジェクト毎の配置位置情報および描画用オブジェクト
毎に必要な画像生成機能を解析して、解析結果情報を生成する解析部と、異なる画像生成機能
を有し、描画用オブジェクトの画像データから印刷用画像データを生成可能な複数の画像生成部と、
解析結果情報に基づいて、描画用オブジェクトの画像データおよび配置位置情報を、それぞれ
必要な画像生成機能を有する画像生成部に振り分ける振分部と、複数の画像生成部において
生成された複数の印刷用画像データおよび対応する配置位置情報を収集して、解析結果情報に
基づいて、ページ単位に印刷用画像データを合成して、ページ画像を生成する合成部と、ページ
画像を印刷する印刷部と、を有する。【選択図】図13"
    ],
    [
      "JP20120024523",
      "画像形成装置、その制御方法、及びプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】複数ページの画像出力を行う際に、当該複数ページ間の出力形態に
応じた関連性をプレビュー表示する画像形成装置、その制御方法、及びプログラムを提供する。
【解決手段】本画像形成装置は、印刷データの設定情報から、ページ間における出力形態に
応じた所定の関連性を解析し、所定の関連性が特定されたページについては、当該所定の関連性
を有する複数のページをグループとしたプレビュー画像を生成し、所定の関連性が特定され
なかったページについては、当該ページのみのプレビュー画像を生成し、プレビュー画像を
ページ順に表示部に表示する。【選択図】図3"
    ],
    [
      "JP20110015400",
      "画像形成装置、画像形成装置の制御方法、プログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】 複数のレコードを含むVDPジョブにおいて有効な試し印刷が可能
な画像形成装置を提供する。【解決手段】 VDPジョブを処理するプリンタ103であって、
試し印刷ジョブであると判定されたVDPジョブに含まれる複数のレコードのうち一部の
レコードを試し印刷をプリンタ部407に行わせ、一部のレコードの印刷が行われた後に、
印刷されていないレコードの印刷指示を受け付け、受け付けた印刷指示に応じて、試し印刷
により印刷されていないレコードの印刷をプリンタ部407に行わせる制御部403と有する。
【選択図】 図5"
    ],
    [
      "JP20140008836",
      "印刷制御装置の制御方法、印刷制御装置の制御プログラム、および印刷制御装置の制御プログラムを記録したコンピューター読み取り可能な記録媒体",
      [
        "コニカミノルタ株式会社"
      ],
      "【要約】【課題】クライアントPCのユーザーが、編集対象のジョブを簡単に見つける
ことを可能にする印刷制御装置の制御方法を提供する。【解決手段】本発明の印刷制御装置の
制御方法は、印刷制御装置の記憶部に記憶されている一のジョブについて、ラスタライズ処理
を実行してジョブ識別用の画像データを生成するステップS107と、ジョブ識別用の画像
データが生成された後、記憶部に記憶されているジョブの中に、ジョブ識別用の画像データが
未生成のジョブがあるか否かを判断するステップS108と、ジョブ識別用の画像データが
未生成のジョブがあると判断される場合、ステップS107およびステップS108を繰り返す
一方で、ジョブ識別用の画像データが未生成のジョブがないと判断される場合、ジョブ識別用の
画像データが生成済みのジョブについて、ラスタライズ処理を実行してジョブ編集用の画像
データを生成するステップS111を有する。【選択図】図8"
    ],
    [
      "JP20110137220",
      "画像形成装置、画像形成装置の制御方法、及びプログラム",
      [
        "キヤノン株式会社"
      ],
      "【要約】【課題】PDLデータの複数ページを並列で処理する画像形成装置における
PDLデータの処理状況を適切にユーザに提示すること。【解決手段】PDLデータの複数
ページを並列で処理する画像形成装置101は、出力モードとして、印刷データの処理と
印刷出力を並行して実行する第1出力モード(RIP while Print)、又は、全ページについて
印刷データの処理が完了してから印刷出力を実行する第2出力モード(RIP then Print)の
設定入力をユーザから受け付けて、不揮発性メモリに保持しておく。そして、画像形成装置
101は、出力モードが第1出力モードの場合、PDLデータの処理が完了したページの中で、
連続出力可能ページ数として1ページ目から連続したページ番号の最大値を特定できる情報を
ユーザに提示する。また、出力モードが第2出力モードの場合、展開済ページ数としてPDL
データの処理が完了しているページの数を特定できる情報をユーザに提示する。【選択図】図1"
    ]
  ]
]

このプラグインでは元のテーブルとカラムを指定することにより、テーブルの内容を出力することができます。

  • 元の文書2
> select ftext --filter "_id == 10876408" --limit 1 --offset 0
--output_columns app_id,title,company,abstract --output_pretty yes
[
  [0,1449913923.39666,0.000921726226806641],
  [
    [
      [1],
      [
        ["app_id","ShortText"],
        ["title","LongText"],
        ["company","company"],
        ["abstract","LongText"]
      ],
      [
        "JP20100000015",
        "心室の拡張機能を改善するためのインビボ装置",
        [
          "コルアシスト カルジオヴァスキュラー リミテッド"
        ],
        "(57)【要約】【課題】生体の臓器または組織に内科的または外科的装置を
接続するために適した接続要素を提供すること。【解決手段】薄い織物のパッチの
形態のガードルを含み、そのガードルの側方の端部から延びた複数のタブが反対側
のものと対をなすように配置され、各タブはその反対側のものと結合することができ、
それによりループを形成し、該ループ内に、該臓器または組織と接続されることとな
る装置の一部分を挿入する接続要素。【選択図】図11"
      ]
    ]
  ]
]

*類似文書例2

> word2vec_distance "doc_id:10876408" --sentence_vectors 1 --limit 5 --offset 0
 --n_sort 5 --prefix_filter "doc_id:"
 --table ftext --column app_id,title,company,abstract --output_pretty yes
[
  [0,1449919740.36363,1.23700165748596],
  [
    [5],
    [
      ["app_id","ShortText"],
      ["title","LongText"],
      ["company","company"],
      ["abstract","LongText"]
    ],
    [
      "JP20150021968",
      "心臓病態を治療するための補助及びリコイル機能を備える二相性及び動的調整可能サポートデバイス及び方法",
      [
        "ザ  テキサス  エー  アンド  エム  ユニヴァーシティー  システム",
        "コーイノーヴァ  インコーポレイテッド"
      ],
      "【要約】【課題】心臓の成長及びリモデリングのための、回復及び自然に
潜在的リハビリとなる機械的環境を最適化するように設計された機械的配向デバイス
及び療法を提供する。【解決手段】鬱血性心不全及び関連する心臓病態に罹患している
患者に移植されるように適合した直接心臓接触デバイスであって、心室補助、心室
サポート及び拡張期リコイルを提供する、又は心室サポート及び拡張期リコイル
のみを提供する手段を有する心臓デバイスである。【選択図】図1"
    ],
    [
      "JP20130542174",
      "装着用ガイドルーメン",
      [
        "アビオメド  インコーポレイテッド",
        "タオ  ゼンホン",
        "ボーガン  ステファン",
        "モンゴー  マリー‐イブ"
      ],
      "【要約】心臓内ポンプデバイスの中を第一の開口部から第二の開口部まで
延在しているガイドワイヤ通路を有する該ポンプデバイスと;該ポンプデバイス
の外部に位置する第一の末端を起点に、ポンプデバイスの第一の開口部を通って
該ポンプデバイスの中に入り、ガイドワイヤ通路に沿って延び、第二の開口部を
通って該ポンプデバイスから出て、該ポンプデバイスの外部に位置する第二の
末端まで延在しているルーメンとを含む装置を開示する。ルーメンは、該ルーメン
の中をガイドワイヤが第一の末端から第二の末端まで通るときに該ガイドワイヤが
前記通路に沿って配置されるように、ガイドワイヤを収容すべく構成されている。"
    ],
    [
      "JP20120511994",
      "マルチルーメンカニューレ",
      [
        "ソラテック コーポレーション"
      ],
      "【要約】本出願は、血流を血液ポンプレシピエントに提供するための
方法及び材料に関する。例えば、哺乳動物の循環系に接続し、血液ポンプ(12)
(例えば、補助装置)と共に使用することができる、カニューレ(11)を提供
する。【選択図】図3"
    ],
    [
      "JP20140525200",
      "カウンターパルセーション及び血流導管の結合のための装置、方法、およびシステム",
      [
        "アビオメド  インコーポレイテッド",
        "クン  ボブ",
        "グラッツ  エリック",
        "ザイス  トーステン",
        "シュパニアー  ゲルト",
        "スペンス  ポール",
        "ダウリング  ロブ",
        "ヘイスティー  ケイトリン"
      ],
      "【要約】内腔の第一の部分を画定する第一の導管部分と、内腔の第二の
部分を画定する第二の導管部分とを含む、血流導管を開示する。第一または
第二の導管部分のうち少なくとも一方が先端部分を含み得、第一または第二の
導管部分のうちもう一方が、膨らんだ領域を含み得る。"
    ],
    [
      "JP20110123146",
      "カニューレおよび補助循環装置",
      [
        "国立大学法人 東京大学",
        "ニプロ株式会社"
      ],
      "【要約】【課題】血液の補助循環における循環効率の向上を図ることが
可能なカニューレおよび補助循環装置を提供する。【解決手段】先端31および
基端32,33を有する管状に構成され、先端31側には第1開口部11および
第2開口部21が設けられ、補助循環を行なうために先端31が心臓60に直接
穿刺された状態においては、第1開口部11は大動脈66内に位置し、第2開口部
21は心室64内に位置するカニューレ100は、第1開口部11を含み、
第1開口部31から基端32に向かって延在する第1内腔10と、第1開口部11
よりも基端33側の部分に形成された第2開口部21を含み、第2開口部21から
基端33に向かって第1内腔10と並んで延在する第2内腔20と、を備える。
【選択図】図6"
    ]
  ]
]

Patricia Trieを使ってdoc_id:のベクトルのみを距離演算していますが、130万件ほどの文書に対して1.3secぐらいかかっています。 もし使うなら、もうちょっと高速化したいところだなぁ。

結果はそこそこ分類されていますが、この結果だともうちょっと頑張らないとって感じかなぁ。

全文検索エンジンGroongaからword2vecを簡単に使えるプラグイン

はじめに

Groonga Advent Calendar 2015の11日目の記事です。

GroongaはC/C++で書かれた高速な国産の全文検索エンジンです。

word2vecは、Googleが研究評価用に作った単語の特徴をベクトルで表現しニューラルネットモデルで教師なし学習をさせるツールです。

単語を文脈も考慮させたベクトルで表現しニューラルネットで学習することで、単語の意味的な足し引きができているんじゃないか( kingからman引いてwoman足したらqueenがでてきましたよとか。)ということで少し前に結構流行ったようです。

また、word2vecの作者が簡易的にsentenceのベクトル表現を使えるようにしたword2vecのコードもあるようです。

普段、Groongaを使ったそこそこの規模のデータベースをもっているので、Groongaに格納されたデータからword2vec/sentence2vecで簡単に学習させることができ、また、Groongaから単語ベクトルの演算ができるプラグインを作成しました*1

naoa/groonga-word2vec · GitHub

dump_to_train_fileコマンド Groongaのカラムから学習ファイル生成

Groongaのカラムからword2vecで学習できるように整形してファイルに出力します。

形態素解析やノーマライズ、記号削除などの前処理がオプション指定で行えるようになっています。

sentence_vectorsオプションを使うとdoc_id:とGroongaのテーブルの_idが紐付けられて出力されます。これにより後のベクトル演算時に元のテーブルと関連付ける事が可能です。

また、column名の後にカッコでカテゴリなど特定のカラムに格納されたタグ等に任意のラベルを付与できます。 たとえば、category:カテゴリAやcompany:会社名Aなどの形で学習させることで類似するカテゴリや会社のみを抽出することができます。

実行例

dump_to_train_file docs companies_[company:],categories_[category:],title/,body/@$ \
  --filter 'year >= 2010' --sentence_vectors 1

会社名とカテゴリは、スペースを_でつなげてフレーズ化してラベルをつけ、TitleとBodyは形態素解析、記号削除などをおこなっています。また--filterでテーブルの検索結果のみを出力することができます。

word2vec_trainコマンド word2vecコマンドを実行して学習

これはオプションを少しいじって実行パスにあるword2vecコマンドを実行するだけです。 このプラグインをインストールすると上記のsentence vector追記版のword2vecが自動的にbindirにインストールされます*2。 学習はGroongaを介して実行しなくても構いません。

word2vec_train --window 20 --cbow 0 --threads 12 --iter 10 --sentence_vectors 1

word2vec_distanceコマンド ベクトル距離を演算

word2vecで学習させたバイナリファイルを読み込み、単語ベクトルを演算します。初回のみバイナリファイル読み込んでをメモリに展開するため、少し時間がかかります。Groongaを常駐でサーバ実行している場合は一度読みこめば2度目からは素早く計算することができます。同時に複数のモデルファイルを読み込むことができます(20個まで)。

word2vecに付属しているdistanceコマンドをベースに以下を改良しています。

  • スペースと+/- で単語ベクトルの演算可
  • offset,limit,thresholdなどページ制御のためのオプション
  • sentence_vectorの場合、元のテーブルをひも付けてカラム出力、ソート
  • 高速化のため語彙表を配列ではなくPatricia Trieで保持

元々のdistance.cは語彙表から一致する単語を探すのに線形探索をしていますが、Patricia Trieを使うことによりマッチ時間の短縮が見込めます。

doc_id:や、category:など特定のラベル付けしたもののみを取得するような場合、Patricia Trieで非常に高速な前方一致検索が可能です。

以下の例では、2sec近くかかっていたのが0.01sec以下で類似カテゴリが取得可能となっています。

  • 改良前(全ワード+正規表現"company:.*"で絞込)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \
  --n_sort 5 --white_term_filter "company:.*" --output_pretty yes
[
  [
    0,
    1449782162.00164,
    2.34969544410706
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:google technology holdings",
      0.644274830818176
    ],
    [
      "company:research in motion",
      0.641874372959137
    ],
    [
      "company:htc",
      0.63908725976944
    ],
    [
      "company:lenovo (singapore) pte",
      0.63323575258255
    ],
    [
      "company:blackberry",
      0.622487127780914
    ]
  ]
]
  • 改良後(パトリシアトライで"company:"に前方一致)
> word2vec_distance "company:apple" --is_phrase 1 --limit 5 --offset 0 \
  --n_sort 5 --prefix_filter "company:" --output_pretty yes
[
  [
    0,
    1449781678.28364,
    0.00766372680664062
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:google technology holdings",
      0.644274830818176
    ],
    [
      "company:research in motion",
      0.641874372959137
    ],
    [
      "company:htc",
      0.63908725976944
    ],
    [
      "company:lenovo (singapore) pte",
      0.63323575258255
    ],
    [
      "company:blackberry",
      0.622487127780914
    ]
  ]
]

このようにGroongaは全文検索だけでなくPatricia Trie、Double Array、HashのCライブラリとしても有用に使うことができます。今回のケースでは更新をしないので、Marisa Trieが使えたらより省メモリで構築できてよいかもしれません。

word2vec実行例

会社名のラベルをつけて類似会社名のみを取得してみます。

> word2vec_distance "company:トヨタ自動車株式会社" --limit 10 --offset 0 \
 --n_sort 10 --prefix_filter "company:" --output_pretty yes
[
  [
    0,
    1449815073.42663,
    0.138718605041504
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:日産自動車株式会社",
      0.911168932914734
    ],
    [
      "company:三菱自動車工業株式会社",
      0.891977429389954
    ],
    [
      "company:富士重工業株式会社",
      0.870425820350647
    ],
    [
      "company:ダイハツ工業株式会社",
      0.858096361160278
    ],
    [
      "company:本田技研工業株式会社",
      0.839616179466248
    ]
  ]
]

類似しているぽい会社名が取得できました。

ベクトルの加減算も自由に行えます。

> word2vec_distance "company:トヨタ自動車株式会社 - company:日産自動車株式会社 + company:グーグル_インコーポレイテッド" \
--limit 5 --n_sort 5 --output_pretty yes
[
  [
    0,
    1449815137.37063,
    0.501566171646118
  ],
  [
    [
      5
    ],
    [
      [
        "_key",
        "ShortText"
      ],
      [
        "_value",
        "Float"
      ]
    ],
    [
      "company:グーグル__インコーポレイテッド",
      0.87317681312561
    ],
    [
      "company:グーグル・インコーポレーテッド",
      0.868086099624634
    ],
    [
      "company:ヤフー!_インコーポレイテッド",
      0.836208581924438
    ],
    [
      "company:アリババ・グループ・ホールディング・リミテッド",
      0.827649772167206
    ],
    [
      "company:マイクロソフト_コーポレーション",
      0.825306117534637
    ]
  ]
]

あ、表記ゆれが結構あるな。。

会社名の総数は単語の語彙数よりは数が少ないのでそこそこ高速に検索できています。最初にカテゴリーかなにかで分類すれば、結構高速に得られますね。

sentence2vec実行例

dump_to_train_file Entries title,tag,tags --sentence_vectors 1
word2vec_train --min_count 1 --cbow 1 --sentence_vectors 1
word2vec_distance "doc_id:2" --sentence_vectors 1 --table Entries --column _id,title,tag
[
  [
    0,
    0.0,
    0.0
  ],
  [
    [
      2
    ],
    [
      [
        "_id",
        "UInt32"
      ],
      [
        "title",
        "ShortText"
      ],
      [
        "tag",
        "Tags"
      ]
    ],
    [
      3,
      "Database",
      "Server"
    ],
    [
      1,
      "FulltextSearch",
      "Library"
    ]
  ]
]

doc_idを指定することにより(たぶん)類似する文書の取得でき、そのカラムを直接取得することができます。

実際の実行例のせようと思ってたのですが、min_countオプションを指定するのを忘れてdoc_idが除去されてしまいました。min_conutのデフォルトは5で出現数が5に満たない語彙は捨てられます。sentence vectorの場合は、--min_count 1にして除外されないようにしないといけませんね。実験結果はそのうち載せようと思います。

試してみました。

Groongaからword2vecを使って類似文書を取得してみる - CreateField Blog

おわりに

Groongaからword2vecを簡単に使うためのプラグインを紹介しました。 GroongaはMySQLでテーブル構築やデータ管理ができるMroongaや、PosrgreSQLからGroongaのインデックスを使うことができるPGroongaが開発されています。これらを使えば簡単にGroongaのテーブルを作ることができ、このプラグインを使えばword2vecを簡単に試すことができます。私はMroongaからこのプラグインを利用しています。GroongaやMySQLにデータがあって、わざわざ整形したりするのが面倒という場合はこのプラグインを使ってみてはいかがでしょうか。

word2vecと似たようなものでGloVeというのもあるみたいです。

この発表では編集距離ベースに誤記の表記揺れを抽出した例を紹介しましたが、word2vecをうまく使えば、略語や言い換えなど意味的な表記揺れもある程度取得可能かもしれませんね*3

*1:普通の人はgensimなどでPythonから使いたいでしょうが、なぜかGroonga first脳なので

*2:インストールしないことも可能

*3:ノイズも多いでしょうが

Groongaでのタグ検索と表記揺れとの戦い at Groonga Meatup 2015

Groonga Meatup 2015 - Groonga | Doorkeeper で発表してきました。

www.slideshare.net

英語のタグ検索での表記揺れをTrieで前方一致検索絞込、編集距離(Damerau–Levenshtein distance)、キーボード距離、DFを元に誤記を抽出して対応した話です。

naoa/groonga-term-similar · GitHub

naoa/groonga-tag-synonym · GitHub

ElasticsearchでもDamerauとprefixであいまい検索やっているみたい。 あいまい検索つくってみようかな〜

How to Use Fuzzy Searches in Elasticsearch | Elastic