Claude Media
embeddingバイナリ量子化でVoyage AIの保存コストを1/32にする

embeddingバイナリ量子化でVoyage AIの保存コストを1/32にする

Voyage AI embeddingのoutput_dtypeでfloat/int8/binaryを切り替える方法と、8個の浮動小数点値を1バイトに詰めるオフセットバイナリ方式の変換例をまとめます。

バイナリ量子化で何ができるのか

埋め込み(embedding)は通常、32ビットの精度を持つ浮動小数点数の並びとして返ってきます。この精度をより小さいデータ型に変換する処理を量子化と呼びます。32ビットを8ビット整数に落とせば保存容量は4分の1に、1ビットのバイナリ値まで落とせば32分の1になります。Anthropicは自社の埋め込みモデルを提供していませんが、公式ドキュメントが推奨するVoyage AIのモデルはこの量子化オプションを備えており、output_dtype パラメーターで出力形式を指定します。

保存コストが32分の1になるということは、数百万件規模のベクトルをインデックスするRAGシステムでは、ストレージ費用とメモリ使用量の両方に直接効いてきます。精度と引き換えにこの削減幅を取るかどうかが、量子化を検討する最初の判断ポイントです。

ベクトルデータベースの多くはメモリ上にインデックスを展開して検索を行うため、埋め込みのサイズはディスク容量だけでなくメモリ使用量にも直結します。floatのままでは載り切らない規模のコーパスでも、量子化によってメモリ使用量を抑えられれば、より小さいインスタンスで同じ検索システムを運用できる可能性があります。逆に言えば、量子化を検討する動機は「保存コストを下げたい」というより「同じ予算でより多くのベクトルを扱いたい」という規模の要求から生まれることが多いといえます。

output_dtypeで指定する4つの形式

量子化に対応するVoyageのモデルでは、output_dtype パラメーターで次の4種類を選べます。すべてのモデルが量子化に対応しているわけではなく、利用中のモデルが対応しているかどうかは公式のモデル一覧で個別に確認できます。

形式データサイズ特徴
floatデータサイズ32ビット(4バイト)/ 値特徴既定値。精度・検索精度ともに最も高い
int8 / uint8データサイズ8ビット(1バイト)/ 値特徴符号付きは-128〜127、符号なしは0〜255の範囲
binary / ubinaryデータサイズ1ビット/ 値(8個ずつ1バイトにパック)特徴実際の次元数の1/8の長さの整数リストとして返る

int8uint8 はどちらも1バイトですが、表現できる値の範囲が符号付きか符号なしかで異なります。binaryubinary はさらに踏み込んで、8個の1ビット値を1つの8ビット整数へパックします。返ってくるリストの長さは実際の埋め込み次元数の8分の1になる点に注意が必要です。1024次元の埋め込みをbinary量子化すると、返却されるのは128個の整数になります。

output_dtype を指定するコード側の変更は小さく、embed() 関数の引数を1つ追加するだけです。

import voyageai
 
vo = voyageai.Client()
 
documents = ["Sample text 1", "Sample text 2"]
 
# floatのまま(既定)
result_float = vo.embed(documents, model="voyage-4", input_type="document")
 
# int8に量子化
result_int8 = vo.embed(documents, model="voyage-4", input_type="document", output_dtype="int8")
 
# binaryに量子化(保存サイズは次元数によらず1/32)
result_binary = vo.embed(documents, model="voyage-4", input_type="document", output_dtype="binary")

output_dtype 以外の引数(modelinput_type)はそのままで、量子化の有無だけを切り替えられます。既存のRAGパイプラインに量子化を後から導入する場合も、埋め込み生成部分のコード変更はこの1行で完結します。ただし量子化後のベクトルは値の範囲も型も変わるため、ベクトルデータベース側のスキーマやインデックス設定は別途対応が必要です。

オフセットバイナリ方式の変換例を1バイトで追う

公式ドキュメントのFAQには、具体的な8つの浮動小数点値を使った変換例が載っています。次の8つの埋め込み値を例に考えます。

-0.03955078, 0.006214142, -0.07446289, -0.039001465,
0.0046463013, 0.00030612946, -0.08496094, 0.03994751

バイナリ量子化では、0以下の値をビット0に、正の値をビット1に変換します。この8つの値を左から順に処理すると、次のビット列になります。

0, 1, 0, 0, 1, 1, 0, 1

このビット列を最上位ビットから並べて1つの8ビット整数にパックすると 01001101 になります。ここから先、ubinarybinary で解釈が分かれます。ubinary はこのビット列をそのまま符号なし整数(uint8)として扱うため、値は77になります。binary はオフセットバイナリ方式を使い、uint8 の値から128を引いた符号付き整数(int8)として表現するため、77から128を引いた-51になります。

形式ビット列解釈
ubinaryビット列01001101解釈符号なし整数としてそのまま77
binaryビット列01001101解釈オフセットバイナリ(77 − 128)-51

オフセットバイナリという名前のとおり、実質的にはuint8の値をint8の値域にずらしているだけです。ビット列自体はubinaryとbinaryで共通しており、最終的な整数としての解釈だけが変わります。量子化されたベクトルを自前で比較・検索する実装を書く場合、この変換規則を取り違えるとハミング距離の計算結果が正負反転してしまうため、どちらの形式を使っているかをコード側で明確に管理する必要があります。

どの形式を使うべきか

用途によって適した output_dtype は変わります。

用途おすすめ度理由
検索精度を最優先する小〜中規模データセットおすすめ度◎ float理由精度低下を避けたい場合の既定選択
数百万件規模のベクトルインデックスおすすめ度◎ int8 / binary理由ストレージとメモリの削減効果が大きい
概算検索後に上位候補だけ再ランキングする2段構成おすすめ度◎ binary(1段目)+ float(2段目)理由粗い絞り込みを高速・低コストに行い、最終順位はfloatで精緻化する
レイテンシーがシビアなリアルタイム検索おすすめ度○ int8理由floatほどの精度低下なく高速化できる
法務・医療など誤検索の許容度が低い領域おすすめ度△ float推奨理由量子化による精度低下がリスクに直結しやすい

binary保存コストの削減幅が最大ですが、精度の低下幅も相対的に大きくなります。実務では、binary で候補を大量に絞り込んでから、上位の少数件だけを float の埋め込みで再計算して並べ直す2段構成が使われることがあります。1段目のビット演算は高速なので、大規模なコーパスに対しても現実的な速度で候補を絞り込め、2段目のfloat比較は候補数が少ないぶん計算コストを抑えられます。

Matryoshka埋め込みと組み合わせる場合の注意点

Voyageの一部のモデル(voyage-code-3 など)はMatryoshka学習と呼ばれる手法で埋め込みを生成しており、ベクトルの先頭から必要な次元数だけを切り出して使うことができます。1024次元のベクトルを256次元に切り詰めても、粗から精への情報がベクトルの前方に詰まっているため、有用な埋め込みとして機能します。

次元の切り詰めと量子化は独立した操作で、両方を組み合わせることもできます。ただし切り詰めた後のベクトルは正規化(ノルムを1に揃える処理)をやり直す必要があります。切り詰める前のベクトルはノルムが1になるよう学習されていますが、先頭部分だけを取り出すとノルムが1からずれるため、そのまま量子化やコサイン類似度の計算に使うと精度が落ちます。次元を切り詰めてから量子化する場合は、切り詰め→再正規化→量子化の順で処理することが必要です。

次元削減と量子化は、どちらも「情報量を削って軽くする」という点では似ていますが、削っている対象が違います。次元削減はベクトルの長さそのものを短くする操作で、量子化はベクトルの長さを変えずに1つ1つの値の精度を落とす操作です。両者は掛け合わせられるため、たとえば1024次元のfloatベクトルを256次元に切り詰めたうえでint8量子化する、といった組み合わせも可能です。この場合の削減率は、次元削減による4分の1と、float→int8による4分の1を掛け合わせた16分の1になります。組み合わせる場合は、削減率が掛け算で効いてくる分、精度への影響も両方の操作の影響が重なる点に注意が必要です。

rerankモデルと組み合わせた2段検索

Voyage AIはembeddingモデルとは別に、クエリと文書リストを受け取って関連度順に並べ替えるrerankモデル(rerank-2.5 / rerank-2.5-lite)も提供しています。binary量子化による粗い絞り込みと、rerankモデルによる精緻な並べ替えを組み合わせると、速度・コスト・精度のバランスを取りやすい2段構成になります。

1段目では、binary量子化した埋め込み同士のハミング距離を使って、コーパス全体から候補を数百件程度まで高速に絞り込みます。ビット演算による距離計算はfloatのコサイン類似度計算よりもはるかに軽量なので、コーパスが大規模でも1段目の絞り込みは実用的な速度で完了します。2段目では、絞り込んだ候補だけをrerankモデルに渡し、クエリとの関連度で最終的な順位を決めます。rerankモデルはクエリと文書のペアを直接評価するため、埋め込みだけの類似度検索よりも高い精度が期待できますが、候補全件に対して実行するにはコストが見合いません。候補を絞り込んでからrerankを適用することで、精度とコストの両方を成立させやすくなります。

この構成は、埋め込みの量子化とrerankモデルという2つの独立した最適化手段を組み合わせているだけなので、どちらか一方だけを先に導入し、効果を見ながらもう一方を追加するという段階的な導入もできます。

Claude RAGパイプラインにどう組み込むか

RAGパイプラインで埋め込みを使う場合、input_type パラメーターに document(格納する文書側)か query(検索クエリ側)かを指定することが推奨されています。量子化を使う場合もこの指定は変わらず必要で、output_dtypeinput_type は独立したパラメーターとして併用できます。

大規模なコーパスを一括で埋め込む処理では、ドキュメント数が数万〜数百万件に達することも珍しくありません。この規模になると、floatのまま全件を保存するとストレージコストが無視できない水準になりやすく、int8やbinaryへの量子化を検討する動機が生まれます。一方でクエリ側の埋め込みは検索のたびに1件だけ生成するものなので、クエリ側の保存コストを気にする必要は基本的にありません。量子化を検討する優先順位は、まず保存件数の多いドキュメント側から、というのが実務上の目安になります。クエリ側は都度生成してすぐに破棄する一時的なデータなので、floatのまま計算しても保存コストには影響しません。RAGパイプライン全体の構築手順はClaude RAGの構築ガイド、取りこぼしを減らす前処理はContextual Retrievalにまとめています。

まとめ

output_dtypefloat から int8binary に切り替えることで、埋め込みの保存コストを4分の1から32分の1まで削減できます。binaryubinary はビット列自体は共通で、オフセットバイナリ方式による整数としての解釈だけが異なります。精度低下のリスクを抑えたい場合は、binaryで粗く絞り込んでからfloatで再ランキングする2段構成、あるいはrerankモデルと組み合わせる構成が現実的な選択肢です。Matryoshka埋め込みの次元切り詰めと組み合わせる場合は、切り詰め後の再正規化を忘れないようにする必要があります。

量子化はコードの変更量としては output_dtype を1つ追加するだけの小さな作業ですが、効果はベクトルデータベースのスキーマ設計やインデックス構成にまで及びます。導入する際は、まず一部のコレクションやコーパスの一部だけで量子化を試し、検索精度の変化を確認してから全体に適用する段階的な進め方が手堅いといえます。API全体の構成はAnthropic API完全ガイドで確認できます。

この記事を共有:XはてブLinkedIn