Microsoft Build 2024 でのアップデートで Azure AI Search においてバイナリ ベクトル型での保存・検索が可能になりました。今回は Azure Machine Learning/Azure AI Studio のモデルカタログを使用して Cohere-embed-v3-multilingual をデプロイして Azure AI Search でバイナリ ベクトル検索を実施します。
デプロイ
Cohere-embed-v3-multilingual でバイナリ ベクトルを取得
response = co.embed(
texts=[text],
input_type=input_type,
embedding_types=["ubinary"]
)
response.embeddings.ubinary[0]
embedding_types=["ubinary"]
と指定することで、uint8
にパックされたバイナリ ベクトルが取得できます。
array([218, 123, 28, 17, 74, 23, 231, 217, 255, 214, 10, 218, 129,
239, 189, 99, 114, 159, 251, 38, 28, 104, 124, 236, 107, 179,
69, 72, 105, 173, 64, 183, 210, 74, 117, 176, 173, 182, 57,
241, 16, 247, 28, 27, 4, 135, 128, 2, 207, 230, 139, 9,
67, 46, 248, 103, 8, 40, 45, 51, 71, 150, 142, 49, 78,
113, 68, 177, 132, 254, 152, 128, 131, 32, 215, 87, 174, 201,
31, 131, 32, 122, 221, 187, 245, 84, 22, 204, 236, 192, 40,
94, 46, 98, 79, 89, 9, 67, 191, 170, 153, 219, 216, 64,
166, 144, 88, 197, 211, 8, 63, 21, 129, 92, 123, 74, 24,
44, 79, 6, 102, 81, 239, 111, 67, 158, 104, 38])
この長さ 128 の uint8
配列をそのまま Azure AI Search に投入できます。
Cohere Embeddings モデルの仕様
バイナリ Embeddings は、1024 個の float32 値を 1024 ビット値に変換し、メモリを 32 分の 1 に削減します。1024 ビットの転送と保存は非効率であるため、これらの 1024 ビット値は 128 バイトにパックされ、符号付き int8 値または符号なし uint8 値として取得できます。
(https://cohere.com/blog/int8-binary-embeddings)
この手法を用いれば、OpenAI の Embeddings モデルもバイナリベクトル化できます。
【参考】 ビットのパックとは
ビットの圧縮手法として、Packbits という手法が使えます。2 値配列 (要素が 0, 1 のみの配列) の要素を、uint8
配列のビットに詰め込みます。解凍には unpackbits
が使えます。
import numpy as np
array = np.array([0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0], dtype=np.uint8)
packed_bin = np.packbits(array)
packed_bin
array([ 0, 255, 128], dtype=uint8)
インデックス作成
Azure AI Search の Collection(Edm.Binary)
フィールドは、1 次元あたり 1 ビットのバイナリ ベクトルを値あたり 8 ビットの uint8
値でパックして保持します。たとえば、元の埋め込みの次元が 1024 である場合、パックされたバイナリ ベクトルの長さは ceiling(1024 / 8) = 128
になります。
fields
type
を Collection(Edm.Binary)
、vectorEncoding
を packedBit
に設定する必要があります。dimensions
に注意してください。実際の配列の要素数ではありません。解凍後の次元数です。
"fields": [
{
"name": "Id",
"type": "Edm.String",
"key": true,
"searchable": true
},
{
"name": "my-binary-vector-field",
"type": "Collection(Edm.Byte)",
"vectorEncoding": "packedBit",
"dimensions": 1024,
"vectorSearchProfile": "myHnswProfile"
}
]
algorithms
バイナリ ベクトル フィールドの場合、ベクトル比較はハミング距離 hamming
を使います。アルゴリズムは HNSW
および Exhaustive-KNN
で使用できます。
"algorithms": [
{
"name": "vector-config-1716115650482",
"kind": "hnsw",
"hnswParameters": {
"metric": "hamming",
"m": 4,
"efConstruction": 400,
"efSearch": 500
},
"exhaustiveKnnParameters": null
}
],
ハミング距離(Hamming Distance)は、2 つの同じ長さの文字列(またはビット列)の間で、対応する位置の文字が異なる数を表す指標です。具体的には、2 つの文字列の各位置を比較し、異なる文字の数を数えます。
a = np.array([1, 1, 0, 1])
b = np.array([1, 0, 1, 1])
np.count_nonzero(a != b)
2
vectorizer
vectorizer を指定すれば自動的に Embeddings に変換してベクトル検索できます。
"vectorizers": [
{
"name": "vector-1716106525833-vectorizer",
"kind": "customWebApi",
"azureOpenAIParameters": null,
"customWebApiParameters": {
"httpMethod": "POST",
"uri": "[FunctionURI]/api/embeddings?embedding_types=ubinary&code=xxx",
"timeout": "PT3M50S",
"authResourceId": "",
"httpHeaders": {},
"authIdentity": null
},
"aiServicesVisionParameters": null,
"amlParameters": null
}
],
インデックス登録
REST API は 2024-05-01-preview
バージョンの CreateOrUpdateIndex を使用。Python SDK は 11.6.0b4
を使用して直接挿入しました。
ベクトル検索
vectorizer 使用
vectorizer をセットした場合は、VectorizableTextQuery
にクエリーテキストを指定して検索できます。
query = "ABC"
vector_query = VectorizableTextQuery(text=query, k_nearest_neighbors=top, fields=vector_field, exhaustive=False)
docs = search_client.search(
search_text="",
vector_queries= [vector_query],
select=["text,docid"],
)
ベクトル配列
従来通り配列を与える場合は VectorizedQuery
を用います。
vector = [26, 126, ...]
vector_query = VectorizedQuery(kind="vector", vector=vector, k_nearest_neighbors=top, fields=vector_field)
docs = search_client.search(
search_text="",
vector_queries= [vector_query],
select=["text,docid"],
)
検索スコア
@search.score
にはハミング距離 hamming
が使われますが、0.007194245
のような値が返ります。これは cosine
と同様、1 / (1 + distance)
のスケーリングが掛けられていますね。これはハミング距離が増加するとスコアが単調に減少するようなスケーリング変換です。自分でベクトルを unpackbits
して計算したハミング距離を 1 / (1 + distance)
することで等しくなることを確認できます。
閾値の設定
ハミング距離に閾値を設定するには、以下のように変換すれば分かりやすいですね。
def score_to_hamming_distance(x):
if x <= 0:
raise ValueError("x must be greater than 0")
return (1 - x) / x
精度比較
前回同様、miracl 日本語 QA データ 8,066 件による比較。
float32 | int8 | binary | |
---|---|---|---|
#1 Recall@3 avg | 0.652 | 0.668 | 0.672 |
#1 Recall@5 avg | 0.786 | 0.786 | 0.751 |
#2 Recall@3 avg | 0.686 | 0.696 | 0.649 |
#2 Recall@5 avg | 0.817 | 0.813 | 0.778 |
#1 Recall@3 以外はバイナリにすると精度が落ちます。これは大幅なベクトルメモリーの削減やパフォーマンス向上とのトレードオフですね。ただバイナリはリランクと組み合わせる手法によって精度向上も可能なので、こちらも試したいです。
パフォーマンス比較
制限
現在はバイナリ ベクトル型へのスカラー量子化機能やリランク機能は実装されていません。
参考