ベクトルの可視化
この例では、milvusの埋め込み(ベクトル)をt-SNEを用いて可視化する方法を示します。
t-SNEのような次元削減技術は、複雑な高次元データを局所構造を保持したまま2次元または3次元空間で可視化するのに非常に有効です。これにより、パターン認識が可能になり、特徴の関係の理解が深まり、機械学習モデルの結果の解釈が容易になります。さらに、クラスタリング結果を視覚的に比較することでアルゴリズム評価を支援し、専門家以外の聴衆へのデータ提示を簡素化し、低次元の表現で作業することで計算コストを削減することができる。これらのアプリケーションを通じて、t-SNEはデータセットに対するより深い洞察を得るのに役立つだけでなく、より多くの情報に基づいた意思決定プロセスをサポートします。
準備
依存関係と環境
$ pip install --upgrade pymilvus openai requests tqdm matplotlib seaborn
この例では、OpenAIのエンベッディングモデルを使用します。OPENAI_API_KEYを環境変数として用意してください。
import os
os.environ["OPENAI_API_KEY"] = "sk-***********"
データの準備
Milvusドキュメント2.4.xのFAQページをRAGのプライベートナレッジとして使用します。
zipファイルをダウンロードし、milvus_docs
フォルダにドキュメントを展開する。
$ wget https://github.com/milvus-io/milvus-docs/releases/download/v2.4.6-preview/milvus_docs_2.4.x_en.zip
$ unzip -q milvus_docs_2.4.x_en.zip -d milvus_docs
フォルダmilvus_docs/en/faq
からすべてのマークダウン・ファイルをロードする。各ドキュメントについて、私たちは単に "# "を使ってファイル内のコンテンツを区切るだけで、マークダウン・ファイルの各主要部分のコンテンツを大まかに区切ることができる。
from glob import glob
text_lines = []
for file_path in glob("milvus_docs/en/faq/*.md", recursive=True):
with open(file_path, "r") as file:
file_text = file.read()
text_lines += file_text.split("# ")
埋め込みモデルの準備
埋め込みモデルを準備するために、OpenAIクライアントを初期化します。
from openai import OpenAI
openai_client = OpenAI()
OpenAIクライアントを使って、テキスト埋め込みを生成する関数を定義します。例として、text-embedding-3-largeモデルを使います。
def emb_text(text):
return (
openai_client.embeddings.create(input=text, model="text-embedding-3-large")
.data[0]
.embedding
)
テスト埋め込みを生成し、その次元と最初の数要素を表示する。
test_embedding = emb_text("This is a test")
embedding_dim = len(test_embedding)
print(embedding_dim)
print(test_embedding[:10])
3072
[-0.015370666049420834, 0.00234124343842268, -0.01011690590530634, 0.044725317507982254, -0.017235849052667618, -0.02880779094994068, -0.026678944006562233, 0.06816216558218002, -0.011376636102795601, 0.021659553050994873]
Milvusにデータをロードする。
コレクションの作成
from pymilvus import MilvusClient
milvus_client = MilvusClient(uri="./milvus_demo.db")
collection_name = "my_rag_collection"
MilvusClient
の引数として:
uri
をローカルファイル、例えば./milvus.db
とするのが最も便利です。- データ規模が大きい場合は、dockerやkubernetes上に、よりパフォーマンスの高いMilvusサーバを構築することができます。このセットアップでは、サーバの uri、例えば
http://localhost:19530
をuri
として使用してください。 - MilvusのフルマネージドクラウドサービスであるZilliz Cloudを利用する場合は、Zilliz CloudのPublic EndpointとApi keyに対応する
uri
とtoken
を調整してください。
コレクションが既に存在するか確認し、存在する場合は削除します。
if milvus_client.has_collection(collection_name):
milvus_client.drop_collection(collection_name)
指定したパラメータで新しいコレクションを作成します。
フィールド情報を指定しない場合、Milvusは自動的にプライマリキー用のデフォルトid
フィールドと、ベクトルデータを格納するためのvector
フィールドを作成します。予約されたJSONフィールドは、スキーマで定義されていないフィールドとその値を格納するために使用されます。
milvus_client.create_collection(
collection_name=collection_name,
dimension=embedding_dim,
metric_type="IP", # Inner product distance
consistency_level="Strong", # Strong consistency level
)
データの挿入
テキスト行を繰り返し、エンベッディングを作成し、milvusにデータを挿入します。
ここに新しいフィールドtext
、コレクションスキーマで定義されていないフィールドです。これは、予約されたJSONダイナミックフィールドに自動的に追加され、高レベルでは通常のフィールドとして扱うことができます。
from tqdm import tqdm
data = []
for i, line in enumerate(tqdm(text_lines, desc="Creating embeddings")):
data.append({"id": i, "vector": emb_text(line), "text": line})
milvus_client.insert(collection_name=collection_name, data=data)
Creating embeddings: 100%|██████████| 72/72 [00:20<00:00, 3.60it/s]
{'insert_count': 72, 'ids': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71], 'cost': 0}
ベクトル検索で埋め込みを可視化する
このセクションでは、milvus検索を実行し、クエリベクトルと検索されたベクトルを一緒に縮小次元で可視化する。
クエリのデータを取得する
検索用の質問を用意しましょう。
# Modify the question to test it with your own query!
question = "How is data stored in Milvus?"
コレクションで質問を検索し、セマンティックトップ10マッチを取得します。
search_res = milvus_client.search(
collection_name=collection_name,
data=[
emb_text(question)
], # Use the `emb_text` function to convert the question to an embedding vector
limit=10, # Return top 10 results
search_params={"metric_type": "IP", "params": {}}, # Inner product distance
output_fields=["text"], # Return the text field
)
クエリの検索結果を見てみましょう。
import json
retrieved_lines_with_distances = [
(res["entity"]["text"], res["distance"]) for res in search_res[0]
]
print(json.dumps(retrieved_lines_with_distances, indent=4))
[
[
" Where does Milvus store data?\n\nMilvus deals with two types of data, inserted data and metadata. \n\nInserted data, including vector data, scalar data, and collection-specific schema, are stored in persistent storage as incremental log. Milvus supports multiple object storage backends, including [MinIO](https://min.io/), [AWS S3](https://aws.amazon.com/s3/?nc1=h_ls), [Google Cloud Storage](https://cloud.google.com/storage?hl=en#object-storage-for-companies-of-all-sizes) (GCS), [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs), [Alibaba Cloud OSS](https://www.alibabacloud.com/product/object-storage-service), and [Tencent Cloud Object Storage](https://www.tencentcloud.com/products/cos) (COS).\n\nMetadata are generated within Milvus. Each Milvus module has its own metadata that are stored in etcd.\n\n###",
0.7675539255142212
],
[
"How does Milvus handle vector data types and precision?\n\nMilvus supports Binary, Float32, Float16, and BFloat16 vector types.\n\n- Binary vectors: Store binary data as sequences of 0s and 1s, used in image processing and information retrieval.\n- Float32 vectors: Default storage with a precision of about 7 decimal digits. Even Float64 values are stored with Float32 precision, leading to potential precision loss upon retrieval.\n- Float16 and BFloat16 vectors: Offer reduced precision and memory usage. Float16 is suitable for applications with limited bandwidth and storage, while BFloat16 balances range and efficiency, commonly used in deep learning to reduce computational requirements without significantly impacting accuracy.\n\n###",
0.6210848689079285
],
[
"Does the query perform in memory? What are incremental data and historical data?\n\nYes. When a query request comes, Milvus searches both incremental data and historical data by loading them into memory. Incremental data are in the growing segments, which are buffered in memory before they reach the threshold to be persisted in storage engine, while historical data are from the sealed segments that are stored in the object storage. Incremental data and historical data together constitute the whole dataset to search.\n\n###",
0.585393488407135
],
[
"Why is there no vector data in etcd?\n\netcd stores Milvus module metadata; MinIO stores entities.\n\n###",
0.579704999923706
],
[
"How does Milvus flush data?\n\nMilvus returns success when inserted data are loaded to the message queue. However, the data are not yet flushed to the disk. Then Milvus' data node writes the data in the message queue to persistent storage as incremental logs. If `flush()` is called, the data node is forced to write all data in the message queue to persistent storage immediately.\n\n###",
0.5777501463890076
],
[
"What is the maximum dataset size Milvus can handle?\n\n \nTheoretically, the maximum dataset size Milvus can handle is determined by the hardware it is run on, specifically system memory and storage:\n\n- Milvus loads all specified collections and partitions into memory before running queries. Therefore, memory size determines the maximum amount of data Milvus can query.\n- When new entities and and collection-related schema (currently only MinIO is supported for data persistence) are added to Milvus, system storage determines the maximum allowable size of inserted data.\n\n###",
0.5655910968780518
],
[
"Does Milvus support inserting and searching data simultaneously?\n\nYes. Insert operations and query operations are handled by two separate modules that are mutually independent. From the client\u2019s perspective, an insert operation is complete when the inserted data enters the message queue. However, inserted data are unsearchable until they are loaded to the query node. If the segment size does not reach the index-building threshold (512 MB by default), Milvus resorts to brute-force search and query performance may be diminished.\n\n###",
0.5618637204170227
],
[
"What data types does Milvus support on the primary key field?\n\nIn current release, Milvus supports both INT64 and string.\n\n###",
0.5561620593070984
],
[
"Is Milvus available for concurrent search?\n\nYes. For queries on the same collection, Milvus concurrently searches the incremental and historical data. However, queries on different collections are conducted in series. Whereas the historical data can be an extremely huge dataset, searches on the historical data are relatively more time-consuming and essentially performed in series.\n\n###",
0.529681921005249
],
[
"Can vectors with duplicate primary keys be inserted into Milvus?\n\nYes. Milvus does not check if vector primary keys are duplicates.\n\n###",
0.528809666633606
]
]
t-SNEによる2次元への次元削減
t-SNEによって埋め込み要素の次元を2次元に削減しましょう。t-SNE変換を行うために、sklearn
ライブラリを使用します。
import pandas as pd
import numpy as np
from sklearn.manifold import TSNE
data.append({"id": len(data), "vector": emb_text(question), "text": question})
embeddings = []
for gp in data:
embeddings.append(gp["vector"])
X = np.array(embeddings, dtype=np.float32)
tsne = TSNE(random_state=0, max_iter=1000)
tsne_results = tsne.fit_transform(X)
df_tsne = pd.DataFrame(tsne_results, columns=["TSNE1", "TSNE2"])
df_tsne
TSNE1 | TSNE2 | |
---|---|---|
0 | -3.877362 | 0.866726 |
1 | -5.923084 | 0.671701 |
2 | -0.645954 | 0.240083 |
3 | 0.444582 | 1.222875 |
4 | 6.503896 | -4.984684 |
... | ... | ... |
69 | 6.354055 | 1.264959 |
70 | 6.055961 | 1.266211 |
71 | -1.516003 | 1.328765 |
72 | 3.971772 | -0.681780 |
73 | 3.971772 | -0.681780 |
74行×2列
Milvus検索結果の2次元平面上での可視化
クエリベクトルを緑、検索されたベクトルを赤、残りのベクトルを青でプロットする。
import matplotlib.pyplot as plt
import seaborn as sns
# Extract similar ids from search results
similar_ids = [gp["id"] for gp in search_res[0]]
df_norm = df_tsne[:-1]
df_query = pd.DataFrame(df_tsne.iloc[-1]).T
# Filter points based on similar ids
similar_points = df_tsne[df_tsne.index.isin(similar_ids)]
# Create the plot
fig, ax = plt.subplots(figsize=(8, 6)) # Set figsize
# Set the style of the plot
sns.set_style("darkgrid", {"grid.color": ".6", "grid.linestyle": ":"})
# Plot all points in blue
sns.scatterplot(
data=df_tsne, x="TSNE1", y="TSNE2", color="blue", label="All knowledge", ax=ax
)
# Overlay similar points in red
sns.scatterplot(
data=similar_points,
x="TSNE1",
y="TSNE2",
color="red",
label="Similar knowledge",
ax=ax,
)
sns.scatterplot(
data=df_query, x="TSNE1", y="TSNE2", color="green", label="Query", ax=ax
)
# Set plot titles and labels
plt.title("Scatter plot of knowledge using t-SNE")
plt.xlabel("TSNE1")
plt.ylabel("TSNE2")
# Set axis to be equal
plt.axis("equal")
# Display the legend
plt.legend()
# Show the plot
plt.show()
png
見ての通り、クエリーベクトルは検索されたベクトルに近い。検索されたベクトルは、クエリを中心とした一定の半径を持つ標準的な円の中にはありませんが、それでも2D平面上ではクエリベクトルに非常に近いことがわかります。
次元削減技術を使うことで、ベクトルの理解やトラブルシューティングが容易になります。このチュートリアルを通して、ベクトルについての理解が深まることを願っています。