• 締切済み

MySQLでの128次元ベクトルの距離計算高速化

MySQLで128次元ベクトルの距離計算をしたいと考えています。 ※ 距離と書いていますが、見つけたいのは最も近くにあるデータなので厳密な距離である必要はありません。 ただし、登録されているデータ量やクエリの工夫のなさにより速度が出ない(タイムアウトするレベル)状態です。 そこで、高速化する方法についてご享受ください。 【環境】 OS:CentOS mysql:mysql Ver 14.14 Distrib 5.1.58, for redhat-linux-gnu (x86_64) using readline 5.1 DBに登録されているデータ件数:150万件 総データサイズ:1.6GByte(1レコードあたり1k程度と思われます) カラム数:130      id1,id2,pt1,pt2,…,pt128      int(11),int(11),float,float…,float      近似値計算はfloat部分で行います クエリ: select id1, MIN(POWER(pt1-dat1,2) + POWER(pt2-dat2,2) + … POWER(pt128-dat128,2) ) as 'nearest' from testDB group by id1 order by nearest limit 1 ※dat1は実数です 実行時はCPU使用率が100%であるため、計算量がボトルネックになっているのかなと思っています。 これをなんとか高速化する方法はないでしょうか。 SQLのチューニングや設定の見直し、はたまた次元数を減らす方法等なんでも構いません。 不明、不足な点についてはご指摘いただければ追記させて頂きます。 以上です、よろしくお願い致します。

  • nmtkn
  • お礼率38% (5/13)
  • MySQL
  • 回答数8
  • ありがとう数7

みんなの回答

  • ki073
  • ベストアンサー率77% (491/634)
回答No.8

No.5のお礼欄について メモリにデータを読み込んで計算する時ですよね。 データが1GBytes以上ありますので、どうしても読み込むのに時間がかなりかかってしまいます。Webアプリケーション側で計算処理するのでしたら、アプリケーションを立ち上げるときに読み込んでじっとメモリ上に抱えておく方法でしょうねえ。 Rubyなどで計算部分を別アプリケーションにするのであれば、計算アプリケーションを常時立ち上げておき、Webアプリケーション側から計算アプリケーションに対して計算をリクエストすればどうでしょうか。データさえ読み込んでおけばすぐに答えがかえってきます。泥臭いやり方ですが、named pipeなどを使ってリクエストを送って、答えももらえばできるように思います。もっと良い方法があるように思いますが、今ちょっと思いつきません。

  • ki073
  • ベストアンサー率77% (491/634)
回答No.7

No.5でありましたRubyのプログラムを載せておきます。 2つの方法計算しており、その時間を測定するようにしていますので、だいぶ長くなっています。 メモリが3GBytesで仮想メモリを使わずにぎりぎり下の計算ができます。上はもう少し少なくても大丈夫です。 こちらでの結果は、上が10秒、下が2秒程度です。メモリさえあれば下がだいぶ速いです。最近8GBytes増設して4000円弱でしたので、メモリを奮発すれば良いと思います。 narrayという行列計算ライブラリを使っていますので、rubygemsでインストールしておいてください。行列計算がかなり速くなります。 -------------- require "rubygems" require "narray" require "benchmark" # 乱数でデータ作成 dat1=NArray.sfloat(128).random!(100) pt=NArray.sfloat(128, 1500000).random!(100) distance_sq=NArray.sfloat(pt.shape[1]) puts Benchmark::CAPTION puts Benchmark.measure{ # メモリ節約計算 (0...pt.shape[1]).each{|i| distance_sq[i]=((pt[true, i]-dat1)**2).sum } nearest_index1=distance_sq.sort_index[0] puts "nearest_id: #{nearest_index1} distnce: #{Math.sqrt(distance_sq[nearest_index1])}" } puts Benchmark.measure{ # 高速計算?? nearest_index2=((pt-dat1)**2).sum(0).sort_index[0] puts "nearest_id: #{nearest_index2} distnce: #{Math.sqrt(((pt[true, nearest_index2]-dat1)**2).sum)}" }

  • ki073
  • ベストアンサー率77% (491/634)
回答No.6

No.3,5です。 No2のお礼欄の >また、pt1~pt128はid1,id2を複合主キーとして持つデータなので固定 が少し気になるのですが、id1,id2+少数のデータからpt1~pt128が作られるのでしょうか?もしそうであればメモリ上で展開すればメモリ節約になるかも。 さて、三次元程度なら、よくやる方法として、 適当な大きさのセルに分割し、そのなかに入っているデータのリストを作って、隣接するセルを含めて検索する方法があります。三次元の場合は、自分自身と隣接セルで9個ですので効率的なのですが、128次元だと、各次元4分割でも4の128乗でデータ数を遥かに越えてしましますので、全然だめですね。インデックス法も同じ様なことになり非常に難しそうです。(勧めておきながらすみません) 多次元でもできる方法として、クラスタに分けて、各クラスタのデータの中心座標と、中心からの最大距離を求めておき、それを手がかにに検索する方法もあります。しかしこれも塊があちこちにあり、塊自体が独立しているようなものでないと難しそうです。(宇宙の星団のようにスカスカの空間のなかに集団があるようなものに適している) 他には、次元を減らす方法もあります。例えば三次元空間中の直線を考えると、座標軸を回転させることにより一次元目に直線がくるようにすると、残りの次元は無視できます。そんなふうにうまくいけば良いが、通常のデータだと全次元を調べないと答えがでないことが殆どなので、これも難しそうです。 結果的には、メモリ上で全部計算した方が速そうなように思います。

  • ki073
  • ベストアンサー率77% (491/634)
回答No.5

No.3です。 SQLを使わずに普通にプログラムを組んで150万件の128次元のデータを乱数で発生されて試験をしてみましたが、最短距離を見つける部分は10秒もかからず計算できましたが、この方が合理的だと思いますが。 5分くらいの瞬間芸で作ったものでメモリをふんだんに使うものですが(2GBytesは多分必要)、もし良ければ書き込みますが。 (Rubyで書いたもので数行のものです)

nmtkn
質問者

お礼

ご回答ありがとうございます。 ちょうどRubyも勉強中ですので、ぜひお願い致します。 1点教えていただきたいのですが、Webアプリケーションとして現在実装を進めています。(インタフェース部分はPHP) その場合には、クライアントから要求(クエリ)があるたびにHDD内の内容をメモリに展開して検索を行なってしまい、I/O部分がボトルネックになるのでは?と考えています。 このような場合はどういった対処方法があるのでしょうか?

  • mpro-gram
  • ベストアンサー率74% (170/228)
回答No.4

まずは、質問文の SQL で気付く点 1.2乗するだけなら、power(x,2) よりも、x*x の方がずーっと早いのは、どのプログラミング言語もおなじなはず。 2.group by してるけど、どのみち全件で計算がおこなわれるから、よけいな作業が入って遅くなるだけでは?最終的に最短の1件取り出してるだけの様だし。 3.いずれにしても、150万件全部計算してはいられないから、絞る必要がありますね。 一回の SQL文では難しいと思います。 近そうなものを割り出すには、No.3 ki073 さん の方法を、さらに mysql での最適化をするなら、 between を使った方が確実に index を利用します。 とはいえ、128カラム全部の複合index では、本体となんら代わりがない可能性が高いので、1カラムごと128個かな、実際の検索に使われるindexは1個だし、複合indexにするか悩みどころかも。 count(*) の場合には、一番効率のよさそうなindex が使われるらしいです(これは、MySQL 5 以降でinnodb の場合についてだったかな、以下参照) http://nippondanji.blogspot.jp/2010/03/innodbcount.html あと、insert,updateが頻繁にあるなら OPTIMIZE TABLE は、行っておいた方がよさそうです。 で、本題、まずはあたりをつけるSQL文 pt1,pt2 はカラム名、 dat1,dat2 は座標値、 d はその座標からの距離値 select count(*) from `testDB` where ( `pt1` between dat1-d and dat1+d ) and (`pt2` between dat2-d and dat2+d) and ... ; これで、dを変更しつつ、適当な件数(ま、100件くらいは許容範囲として、もし1件だったら、正方形の対角線距離より近いけど正方形の外というのと同じ状況がありうるので、d*2 で絞り直す,次元数が大きいので2倍でよいかは不明)に絞れたら、 この条件をつけたもので距離計算する。power は遅いので、地道に かけ算をする select `id1`, (`pt1`-dat1)*(`pt1`-dat1) + (`pt2`-dat2)*(`pt2`-dat2) + ()*() + ... as `nearest` from `testDB` where (`pt1` between dat1-d and dat1+d) and (`pt2` between dat2-d and dat2+d) and () and ... order by `nearest` limit 1 ; 128カラムもあるとすごく長くなるけど、SQL文の最大長は何バイトだったかな?? 識別子に予約語を使っていなければ、 `` は省略可能、これで770バイトも違ってくる。 http://nippondanji.blogspot.jp/2009/05/mysql.html によれば、「MySQLサーバーが実行出来るSQL文の最大長は、max_allowed_packetシステム変数で表され、max_allowed_packetの最大値は1GB」 そのシステムでの制限値を調べる必要はあるけど、そうとう長くても、全然問題ないようですね。データの桁数にもよるけど、1カラム用の計算式と条件文で100byte 近く必要になったとして、全文で 13KB くらい。 それでもちょっとやそっとでない時間もかかりそうだけど、15万件全部計算するのに比べれば、タイムアウトはしないんじゃないかな?

nmtkn
質問者

お礼

ご回答ありがとうございます。 インデックスはあまり使ったことがなかったので、大変勉強になります。 どの程度の速度が実際に出るものなのか教えていただいた方法を試してみたいと思います。

nmtkn
質問者

補足

補足です。 updateは基本的にはありません。 (deleteもあまりありません) が、insertについてはそれなりに発生します。 抽象的な言葉での申し訳ありません。 なにぶんまだシステムの検討段階でして、技術的な検証を進めている状態です。 ご容赦ください。

  • ki073
  • ベストアンサー率77% (491/634)
回答No.3

調べたい点は1つではなく、複数あるとして考えます。 質問欄にあるのでは、インデックスは効かずに端から端までデータを読んでいるのでものすごく時間がかかるのだと思います。 やり方として 1)まず、pt1~128までのインデックスを作成するようにしておきます。 2)次に調べたい点dat2の近くに1点以上は存在するであろう距離dを適当な値に決めます。 3) (pt1<dat1,2+dかつpt1>dat1,2-d)かつ(pt2<dat2,2+dかつpt2>dat2,2-d)かつ..... のように全ての次元がdat2の±dの範囲にあるデータを探します。これだとインデックスの効果があるはずです。 4) 上の条件を満たす点を集め、全部の距離を計算し、一番近いものを探します。もしdより近いものがなければ,一番近いものが3)の中に必ず有る保証はありませんので、dの値を少し大きくしてもう一度やりなおします。 上記3)で条件式が256個も並ぶので、適当な次元、例えば1~10次元目までで調べ、条件を満たしたもののなかで、1~10次元目だけで距離を計算しdを超える場合には外して、残っているのを対象に次の次元から調べ始めるというのも効率が良いかもしれません。

nmtkn
質問者

お礼

ご回答ありがとうございます。 インデックスをはって誤差範囲内にあるものを探すのは有効と思いました。 データの特性を考えつつ誤差範囲を決めて試してみたいと思います。

  • Siegrune
  • ベストアンサー率35% (316/895)
回答No.2

## POWERって結構CPUのPowerが必要なんですよね。。。 ## って冗談はさておき、 ## ベクトル計算は、普通のコンピュータ、あるいはSQLではあまり得意じゃないんですよね。 ## スパコンとかが向いているし、言語もFORTRANとかが向いているのじゃなかったかな。 ## という今の時点ではどうにもならない感想レベルの話は置いておいて、 「タイムアウトするレベル」を防ぎたいのなら select id1,id2, pt1-dat1 as w1001,pt2-dat2 as w1002, … ,pt128-dat128 as w1128 from testDB を作業用テーブル(w1000)に一度格納して、 select id1,id2, power(w1001,2) as w2001,power(w1002,2) as w2002, … ,power(w1128,2) as w2128 from w1000 ## このSQLは危ない(タイムアウトしそう)かもしれません。 を作業用テーブル(w2000)に格納して、 select id1, MIN(w2001 + w2002 + … ,w2128) as 'nearest' from w2000 group by id1 order by nearest limit 1 とするしかないかと思いますが。 id2毎にデータを持っているのがどういう意図かわからないので、 その意図が分かれば多少効率化できるのかもしれませんが。

nmtkn
質問者

お礼

ご回答ありがとうございます。 id2はid1のデータ順(番号)となります。 イメージとしては、以下の様な感じです。 id1:1,1,2,2,3,4 id2:1,2,1,2,1,1 また、pt1~pt128はid1,id2を複合主キーとして持つデータなので固定(クエリのたびに変動しない)ですが、dat1~128についてはクエリを投げるごとに変わります。 こういった、動的?なものに対しても有効でしょうか?

回答No.1

POWER(pt1-dat1,2)からPOWER(pt128-dat128,2)までの値をテーブル上に持つ方法ではだめですか。

nmtkn
質問者

お礼

ご回答ありがとうございます。 dat128は可変であるため、テーブル上に持つのは難しいと思っています。

関連するQ&A

  • 高次元整数ベクトルの高速計算

    VC++2010でwindowsアプリケーション(CLI)を作成しています。 その中で1000次元の整数ベクトルのパワーを求めるために概略次のような計算をしています。 int v[1000]; //v[0]~v[999]に整数値を代入後 long long int power = 0; for(int i = 0; i < 1000; i++){ power += v[i] * v[i]; } この計算を数万回繰り返す必要があるため、是非高速化を図りたいのですが、CPUのベクトル計算機能等を利用して高速化することができるでしょうか?

  • PHP+MySQLで高速化

    mysql_query()が返したresourceの行数は得られませんか? 例をあげると データが id p (フィールド) 42 651 23 357 67 123 28 385 このように4レコード有ったとして $res = mysql_query('select p from tbl where p="123"'); これで、$resから3行目というのを得て、3を使ってidの67を得たいのですが、 $res = mysql_query('select id,p from tbl where p="123"'); のようにして$resのidを見るようにする以外の方法は無理なんでしょうか? もし前者が可能ならそちらの方が速いと思うのですが。

    • ベストアンサー
    • PHP
  • MySQL「10行を超えたら削除する」について

    初心者と言うより、全く全くわからない状態で質問することご容赦下さい。ブログ調の掲示板で、MySQLを使っておりますが、掲示板内の投稿が10投稿を超えると、古いものから順に削除されていく状態のようで、そのリミットを完全に解除したいと思い、ファイルを探していたら、 //10行超えたら削除していく $result = mysql_query("select * from bbs_data where bbs_user_id='{$urow[id]}' order by id desc limit 9,1") or die(mysql_error()); if(mysql_num_rows($result) == 1){ $row = mysql_fetch_assoc($result); $result = mysql_query("delete from bbs_data where bbs_user_id='{$urow[id]}' and id < '{$row[id]}'") or die(mysql_error()); } という部分がありました。このリミットを解除するにはここのプログラムを変えればいいのでしょうか? その場合、どのようにしたらいいか教えて下さい。

    • ベストアンサー
    • MySQL
  • phpとmysqlでデータの一覧表示をしたいと思っています。

    phpとmysqlでデータの一覧表示をしたいと思っています。 ずらーっと並べるだけでなく、1ページに100件した場合には 10行毎に<hr>や<br>などのタグを挿入して間隔を空けたいのですが どのようにしたらいいのか悩んでいます。 現在は下記のようにLIMITで何度もqueryを発行しています。 できれば1回のqueryで処理したいのですが 他にどのような方法があるでしょうか。 $rs = mysql_query("select * from data order by id desc LIMIT 0,10 ;",$conn); while($rec = mysql_fetch_array($rs, MYSQL_ASSOC)){ $site = $rec['site']; $url = $rec['url']; echo '<a href="'.$url.'">'.$site.'</a>'; } $rs = mysql_query("select * from data order by id desc LIMIT 10,10 ;",$conn); while($rec = mysql_fetch_array($rs, MYSQL_ASSOC)){ $site = $rec['site']; $url = $rec['url']; echo '<a href="'.$url.'">'.$site.'</a>'; } $rs = mysql_query("select * from data order by id desc LIMIT 20,10 ;",$conn); while($rec = mysql_fetch_array($rs, MYSQL_ASSOC)){ $site = $rec['site']; $url = $rec['url']; echo '<a href="'.$url.'">'.$site.'</a>'; } よろしくお願い致します。

    • ベストアンサー
    • MySQL
  • phpでmysqlからデータを取り出して一覧表示

    phpでmysqlからデータを取り出して一覧表示させるプログラムを造りたいのですが、どうもうまくいきません。まずはデータの取り出し表示の仕方を教えてください。 $sqlstr="select * from webdiary where username=$id order by topicid asc"; $result = mysql_query( $sqlstr ); この後どのようにしたらよいでしょうか?

    • ベストアンサー
    • PHP
  • PHPでMySQLのデータを2次元配列に格納する

    PHPの本に $r = mysql_query("SELECT * FROM tb;"); while ($row = mysql_fetch_array($r)){ print "{$row['id']}{$row['title']}<BR>"; } とあったのですが1行ずつしか保存できないので 2次元配列row[][]で while ($row[] = mysql_fetch_array($r)){ } foreach($row as $v){ print "{$v['id']}{$v['title']}<BR>"; } このようにコーディングしたところ 一応うまく表示されたのですが、問題はないですか? もし普通はこういう風にするみたいなやり方があれば 教えて欲しいです。

    • ベストアンサー
    • PHP
  • 2次元配列の作り方

    データ型 name varchar(30), item1 char(8), item2 int(6) として下記のテーブルがあります。 | name | item1 | item2 | | taro | abcd | 28 | | taro | efghk | 33| | taro | lmnp | 05 | これから2次元配列形式でデータを取得したいのですが、うまく並んでくれません。 まず、 (前略) $query ="SELECT item1,item2 from *** where name='taro'"; $result = mysql_query($query,$conn_id) or die($query.'failed('.mysql_error().')'); $data[][] = mysql_fetch_assoc($result);  とすると(当然ながら)、 Array ( [0] => Array ( [0] => Array ( [item1] => abcd [item2] =>28 ) ) ) と最初のレコードだけが取得されます。 while($data[][] = mysql_fetch_assoc($result)){ print_r($data); echo "<br/>\n"; } とすると、 Array ( [0] => Array ( [0] => Array ( [item1] =>abcd [item2] => 28 ) ) ) Array ( [0] => Array ( [0] => Array ( [item1] => abcd [item2] => 28 ) ) [1] => Array ( [0] => Array ( [item1] => efghk [item2] => 33 ) ) ) Array ( [0] => Array ( [0] => Array ( [item1] => abcd [item2] => 28 ) )      [1] => Array ( [0] => Array ( [item1] => efghk [item2] => 33 ) )      [2] => Array ( [0] => Array ( [item1] => lmnp [item2] => 05 ) ) ) となってしまうのですが、 Array ( [0] => Array ( [0] => Array ( [item1] => abcd [item2] => 28 ) )      [1] => Array ( [0] => Array ( [item1] => efghk [item2] => 33 ) )      [2] => Array ( [0] => Array ( [item1] => lmnp [item2] => 05 ) ) ) だけを取得させるにはどのように書けば良いでしょうか?

    • ベストアンサー
    • PHP
  • mysql_fetch_arrayとテンプレートの使い方

    データベースからランダムに取り出した3つのデータがあります。 取り出したデータにはそれぞれid、name、ageのデータが入っています。 $rs = mysql_query("select * FROM table order by Rand() LIMIT 0,3;",$con); while($rec = mysql_fetch_array($rs, MYSQL_ASSOC)){ $tpl->assign(array( id => $rec[id], name => $rec['name'], age => $rec[age] )); } これだと1種類のデータが3つ連続して表示されてしまいました。 3種類のデータを全部表示するにはどのようにしたらよいのでしょうか? テンプレート(?)の使い方がよく分かっていないので検討違いのことをしている気がしますがよろしくお願いします。

    • ベストアンサー
    • MySQL
  • MySQLのデータをPHPで多次元連想配列にしたい

    MySQLのデータを多次元連想配列にする方法を教えて下さい。 下記のような多次元連想配列のデータがあります。 これと同様のMySQLに登録されたデータから多次元連想配列を作りたいと思います。 $test= array( array("id" => "1","kamoku" => "算数","tensu" => "70"), array("id" => "2","kamoku" => "理科","tensu" => "88"), array("id" => "3","kamoku" => "国語","tensu" => "90"), ); print_r($test); ---print_r($test)の結果--------------------------------- Array ( [0] => Array ( [id] => 1 [kamoku] => 算数 [tensu] => 70 ) [1] => Array ( [id] => 2 [kamoku] => 理科 [tensu] => 88 ) [2] => Array ( [id] => 3 [kamoku] => 国語 [tensu] => 90 ) ) -------------------------------------------------------- これと同様のデータをMySQLに作成します。 テーブル名:test 列名:id,kamoku,tensu MySQLからデータを取得 try { $dbh = new PDO($DSN , $DBUSER , $DBPASS); $query = select * from test $stmt = $dbh->prepare($query); $stmt->execute(); while($result = $stmt->fetch(PDO::FETCH_ASSOC)) { $id = ($result['id']); $kamoku = ($result['kamoku']); $tensu = ($result['tensu']); } } catch(PDOException $e) { print "Error!: " . $e->getMessage() . "<br>"; die(); } このソースの中で何らかの処理をして print_r($test); を実行したときに ---print_r($test)の結果--------------------------------- Array ( [0] => Array ( [id] => 1 [kamoku] => 算数 [tensu] => 70 ) [1] => Array ( [id] => 2 [kamoku] => 理科 [tensu] => 88 ) [2] => Array ( [id] => 3 [kamoku] => 国語 [tensu] => 90 ) ) -------------------------------------------------------- というような、文頭で記載したものと同じ結果を得たいと思います。 while内で print_r($result);を行うと1行ずつ下記のような連想配列 Array ( [id] => 1 [kamoku] => 算数 [tensu] => 70 ) が取得できているのでこれを連結させて $test_sample=<<<EOF array("id" => "1","kamoku" => "算数","tensu" => "70"), array("id" => "2","kamoku" => "理科","tensu" => "88"), array("id" => "3","kamoku" => "国語","tensu" => "90") EOF; という配列の中身は作ることができました。 (1)ケース1 $test=array($test_sample); print_r($test); としてもダメで、 Array ( [0] => array("id" => "1","kamoku" => "算数","tensu" => "70"), array("id" => "2","kamoku" => "理科","tensu" => "88"), array("id" => "3","kamoku" => "国語","tensu" => "90") ) というようにうまく多次元連想配列になっていません。 (2)ケース2 $test='array('.$test_sample.')'; print_r(test); としてみたところ、 array( array("id" => "1","kamoku" => "算数","tensu" => "70"), array("id" => "2","kamoku" => "理科","tensu" => "88"), array("id" => "3","kamoku" => "国語","tensu" => "90")) というように配列ではなく単なる文字列として表示されてしまいます。 配列の中身を変数で扱う時には特別な記述法などがあるのでしょうか?

    • ベストアンサー
    • PHP
  • mysql のテーブルの名前は数字ではだめですか

    phpmyadmin で数字名前のテーブル上手く作りましたが。。なぜかmysqlで $query = "CREATE TABLE 2234( id int AUTO_INCREMENT key, 入力日 VARCHAR(10 ) $squery1 = mysql_query($query); (create table "2234"( ...)もcreat table '2234'(..)もだめ) なぜかエラーが出ました?