HN 日本語サマリー

← 一覧へ戻る
Web開発

ブラウザにおけるセマンティック/ハイブリッド検索

Semantic/Hybrid Search in the Browser (bart.degoe.de)

6 pointsby bartdegoede0 コメント

要約

この記事では、ブラウザ上で動作する軽量なセマンティック検索の実装方法について解説しています。従来のキーワード検索と、サーバーサイドで動作する重厚なセマンティック検索エンジンのギャップを埋めるため、モデルサイズを大幅に削減したlookupテーブル方式のモデル(model2vec)を活用します。これにより、クライアントサイドでAPIサーバーなしに高度な検索機能を提供する方法を探求しています。

全文翻訳

8年前に私はLunr.jsを使ってこのブログにクライアントサイド検索を追加しました。これはビルド時に逆インデックスを作成し、JSONとして配信し、ブラウザで文字列を照合します。サーバーサイドのエンジンは不要です。単語が実際に含まれていれば投稿を見つけられるという意味では、以来うまく機能してきました。 今年の初めに、私はPythonで約250行のセマンティック検索エンジンを書きました(「ロンドンビール洪水」を「イングランドのアルコール飲料災害」で検索すると、ビールがアルコール飲料であり、洪水が災害であることを理解するため、それを見つけられるようなものです)。それは、sentence-transformersがインストールされたマシンと数百メガバイトのPyTorchを必要とします。そのようなものを本番サーバーで提供するには、高価なRAMを備えた強力なマシンが必要です。静的サイトでは実行できないものです。 そのため、このブログにはどこでも動作するが何も理解しないキーワード検索はありましたが、物事を理解するがどこでも実行できないセマンティック検索はありませんでした。この記事は、そのギャップを埋めることについてです。サーバーもAPIもなしに、ブラウザで完全に動作するセマンティック検索で、モデル全体が基本的に4MBのルックアップテーブルです。ベンチマークした3つのモデルすべてを、さらに下に、自分で試すことができます(私はGPUクラスターを維持できません)。 これを行うことで、いくつかのことが明らかになりました。その中でも最も重要なのは、キーワード検索に数年間バグがあったことです。適切な評価を行うことが重要であることが判明しました。 明白なアプローチは23メガバイトかかります Pythonの記事を支えるモデル(all-MiniLM-L6-v2のようなsentence-transformers)は、実際にブラウザで実行できます。Transformers.jsは、そのうちの1つのONNXビルドをダウンロードし、WebAssemblyで実行します。私はそれをベンチマークしました。私のラップトップでは、量子化されたモデルとそのランタイムは、ワイヤレスで23.45MB、ロードして利用可能になるまで約2秒かかり、その後クエリを約18ミリ秒で埋め込みます。 14件の投稿しかないブログに検索ボックスクエリを埋め込むには、23メガバイトは少し多いです。これは、高度な検索を行うためにダウンロードする必要があるバイト数で、高解像度の写真の約ダース分に相当します。機能しますが、一部のアプリケーションでは完全に価値がありますが、ブルームフィルターについて読むためにクリックした人に、特にモバイル接続で、それを押し付けるようなものではありません。 問題は、検索クエリを埋め込むためにトランスフォーマーが必要なわけではないということです。十分なベクトルが必要です。そして、それを取得するための、はるかに安価な方法があります。 静的埋め込みモデルはルックアップテーブルです トリックは、model2vecと呼ばれるモデルのファミリー(特定のモデルは「potion」と呼ばれます)です。これらは実際のsentence-transformerから蒸留されていますが、結果はニューラルネットワークではありません。それはテーブルです。 これがモデルの完全なフォワードパスです。簡略化ではありません。ライブラリのソースから、すべてです。 ids = tokenize(text) # サブワードトークンIDに分割 rows = embedding[ids] # トークンごとに1つのベクトルをルックアップ vector = rows.mean(axis=0) # 平均化 vector = vector / norm(vector) # 単位長に正規化 これだけです。トークン化、トークンごとにルックアップ、平均化、正規化。注意、レイヤー、推論はありません。「モデルを実行する」とは、配列のルックアップと平均化の組み合わせです。potion-base-8Mの場合、テーブルは29,528トークン x 256次元のfloat32(約30MB)で、MiniLMの検索品質の81%に達し、定義上は辞書ルックアップです。 30メガバイトはまだ多すぎますが、テーブルはトランスフォーマーよりも縮小しやすいものです。 インデックスの構築:チャンキングと、決して元が取れないキャッシュ 埋め込みはビルド時に生成されます。私のラップトップでは、hugoを実行するたびに、Pythonスクリプトが投稿をウォークスルーし、フロントマターとコードブロックを削除し、各投稿を重複する600文字程度のチャンクに分割し、各チャンクを埋め込み、ブラウザがユーザーが検索入力ボックスをクリックしたときにダウンロードするファイルにベクトルを書き込みます。 チャンクの分割は、平均化ステップのため、予想以上に重要です。静的埋め込みは、トークンベクトルの平均です。15,000文字の投稿全体を1つのベクトルに平均化すると、「ソフトウェアに関する一般的な英語の文章」を指すものになり、それ以上のものにはなりません。珍しい、特徴的な単語(pydubやmmh3のような用語)は、周囲の何百もの普通の単語に溺れてしまいます。投稿を小さなチャンクに分割すると、これらの信号がシャープに保たれます。これは、一部のモデルが他のモデルを上回る理由の鍵となるため、後で戻ってきます。 また、各チャンクのテキストのハッシュをキーとする埋め込みキャッシュも構築しました。これにより、再埋め込みは変更されたチャンクのみに触れるようになります。これは時間の無駄とトークンの無駄であることが判明しました。このブログのすべてのチャンクを埋め込むのは、数百回のルックアップと平均化です。約10ミリ秒かかります。すでにほぼ瞬時の操作を高速化するためにキャッシュを構築しました(14件のブログ投稿は大したデータ量ではないことが判明しました)。埋め込みが実際のニューラルネットワークフォワードパスであるMiniLMモデルでは、これはより重要になりますが、私はそれを出荷していません。そのため、キャッシュはそこにあり、正しく、無意味ですが、過剰設計が容易であることの思い出として残しておきました。 テーブルの縮小:モデルのストップワードリストは平易な場所にある テーブルが30MBなのはfloat32だからです。それをint8に量子化すると、サイズは4分の1になります。ただし、すべてを同じスケールにクリップできるわけではなく、その理由を理解することが、これを作成する上で私が学んだお気に入りのことの1つでした。 行の大きさ(マグニチュード)が実際にはどうなっているかを見てみましょう。potion-base-8Mのすべてのトークンをベクトルの長さでソートすると、最も短いベクトル(つまり、ゼロに近いもの)は次のようになります。 a . , - ) the to and of in そして最も長いのは次のようになります。 turkmenistan seychelles guantanamo hemingway vanuatu これは偶然ではなく、ノイズでもありません。モデルのストップワードリストは、その行の大きさです。トークンベクトルを一緒に平均化すると、小さなベクトルを持つ単語は結果をほとんど動かさず、大きなベクトルを持つ単語はそれを支配します。モデルは、ストップワードリストや特別な処理なしに、単語「the」はほとんど貢献すべきではなく、「guantanamo」のような単語は大きく貢献すべきであることを学習しました。それは美しいもので、幾何学の中にあります。 だからこそ、量子化には行ごとのスケールが必要です。各トークンは独自のfloat32乗数を持つため、相対的な大きさがint8に押しつぶされても生き残ります。しかし、実際にはあまり重要ではないことが判明しました。 測定しました。単一のグローバルスケールで実際のテーブルを量子化し、クエリベクトルがどれだけ劣化するかを調べました。答えは、ほとんど何もなかったということです。元のベクトルに対する集計コサイン類似度は0.9998のままでした。グローバルスケールは、29,528トークンの語彙全体で正確に2つの行(.とa)をゼロにします。これらは、モデルがすでにほとんど貢献しないと判断した2つのトークンです。メカニズムは本物ですが、この特定のモデルでは重要ではありませんでした。 行ごとのスケールは、4MBのうち118KBしかかからず、正しいので、それらを保持しました。しかし、それは「ロードを支える」というよりは「安価な保険」のようなものです。行ごとのスケールを含め、int8テーブル全体は、元のfloat32モデルをコサイン0.999958で再現します。ルックアップテーブルをモデルの代わりにブラウザに出荷するには十分です。 80行のWordPiece、およびその3つの注意点 ブラウザにはトークンテーブルがありますが、Pythonで行ったのと同じように、入力されたクエリをトークンIDに変換する必要があります。これにより、正しい行をルックアップできます。つまり、BERT WordPieceトークナイザーをJavaScriptで再実装する必要があります。約80行で、3つの注意点があり、それぞれがクエリベクトルを静かに汚染する可能性があります。 [CLS]/[SEP]なし。BERTトークナイザーは通常、テキストを特別なマーカートークンでラップします。tokenizer.json構成には、その方法を説明するセクションさえあります。model2vecはそれを使用しませんが、add_special_tokens=Falseを指定してトークナイザーを呼び出します。マーカーを追加すると、存在しない2つのベクトルを平均化することになります。 未知のトークンは削除され、埋め込まれません。単語が語彙にない場合、model2vecはドロップします