Enhance your RAG Agent with Multimodal Retrieval | Mediumの翻訳です。
本書は著者が手動で翻訳したものであり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
著者: Austin Choi, Sr. Delivery Solutions Engineer @ Databricks and Austin Zaccor, Specialist Solutions Architect @ Databricks
マルチモーダルエンベディングモデルによって、モーダル横断のパワフルな検索機能を可能にします。これらのモデルによって、メタデータやタグなしにテキストのクエリーを用いて画像を検索したり、テキストの説明文に類似した画像を特定することができます。DatabricksのVector SearchやAgent Frameworkでは、上述のエンベディングを生成するためにご自身のマルチモーダルのエンベディングモデルを提供することで、これらの機能をサポートします。
リポジトリはこちらです: https://github.com/auschoi96/vectorsearch_image_embeddings_databricks
エンベディングを選びましょう!
信じようが信じまいが、コンピューターはテキストを読むことはできません。コンピューターは処理の内容を理解するために、数値的表現(古き良き1と0)を参照する必要があります。このため、コンピューターが理解できる数値表現に我々のデータを変換するいくつかの方法を必要とします。
これがエンベディングモデルのゴールです。これらは、あなたのデータを数値表現やエンベディング空間におけるベクトルに変換するためにトレーニングされた機械学習モデルです。
エンベディング空間とは?
エンベディング空間とは、1つ以上のモダリティを浮動小数点のベクトルとして格納できるようにする、レコードのn次元の数学的表現です。何が有用かというと、適切に構成されたエンベディング空間においては、類似した意味を持つレコードが類似した空間を占有するということです。例えば、馬の写真、「トラック」という単語、犬が吠えている録音を想像してみましょう。我々のマルチモーダルエンベディングモデルにこれら3つのまったく異なるデータポイントを入力すると、以下を受け取ります:
- 馬の写真: [0.92, 0.59, 0.17]
- 「トラック」: [0.19, 0.93, 0.81]
- 犬の鳴き声: [0.94, 0.11, 0.86]
実際には、エンベディング空間の次元数は数百や数千になりますが、説明のために3次元を使用します。これらのベクトルの最初は「動物らしさ」であり、2番目が「輸送らしさ」、3番目が「うるささ」であることを想像することができます。これらのエンベディングは合理的なものでありますが、多くの場合どの次元が何を表現するのかを知りません。重要なのは、これらはレコードの意味論的な意味を表現するということです。
マルチモーダルエンベディング空間を作成する方法はいくつか存在しており、複数のエンコーダーの同時トレーニング(CLIPなど)、クロスアテンション機構の活用(DALL-Eなど)、さまざまなトレーニング後の調整手法などがあります。これらの手法によって、レコードの意味をオリジナルのモダリティを超えて、全く異なる他のレコードやフォーマットとの共有空間を占有することができます。
この、共有意味空間によって、モーダル横断のパワフルな検索機能を可能にします。テキストのクエリーや画像が類似したベクトル表現を共有すると、類似した意味を共有する可能性が高く、明示的なタグやメタデータなしにテキストの説明文に基づいて適切な画像を特定することができます。
マルチモーダルエンベディングモデル: エンベディング空間の共有
マルチモーダル検索を効果的に実装するには、共有ベクトル空間においてさまざまなデータタイプのエンベディングを生成できるモデルが必要となります。これらのモデルは特に、異なるモダリティ間の関係性を理解し、統合された数学的空間で表現するように設計されています。
現時点で利用できるいくつかのパワフルなマルチモーダルエンベディングモデルです:
- CohereのMultimodal Embed 3: 高い精度とパフォーマンスでテキストと画像データの両方のエンベディングに秀でた多彩なモデル。
- Nomic-Embed: 統合された空間でさまざまなデータタイプのエンベディングに対する強力な機能を提供。いくつかの完全オープンソースモデルの一つ。
- Meta ImageBind: 画像、テキスト、音声、深さ、温度、IMUデータを含む6つの異なるモダリティを取り扱うことができる印象的なモデル。
- CLIP (Contrastive Language-Image Pre-training): OpenAIによって開発されたCLIPはさまざまな領域の画像とテキストのペアでトレーニングされ、資格的データテキストデータ間のギャップを効果的に埋める。
Databricksにおけるエンドツーエンドのマルチモーダル検索の構築
Databricksは、データ取り込みからデプロイメントに至るマルチモーダル検索機能の実装における包括的なプラットフォームを提供します。Unity Catalogのおかげで、すべては1つの統合されたプラットフォームで管理されるので、ワークフローの全体においてあなたのデータを活用することができます。あなたのクエリーに基づいて画像を検索できるエンドツーエンドのRAGエージェントを作成するために、Databricks Vector Searchをどのように活用できるのかを探索しましょう。
このデモでは以下のことを行います:
- DatabricksのボリュームにCOCO 2017検証データセットの画像をインポートします
- Nomic-Embed-1.5-Visionとテキストエンベディングモデルを用いて、これらの画像をエンベディングに変換し、Deltaテーブルに格納します
- クエリーできるようにDeltaテーブルをVector Searchインデックスに変換します
以下のリソースを作成します:
- COCOデータセットの生画像を格納するUnity Catalogのボリューム
- 画像エンベディングを格納するDeltaテーブル
- 上述のエンベディングを格納するVector Searchインデックス
以下のコードブロックでは実装例を説明しますが、完全なコードはこちらのリポジトリに格納されています: https://github.com/auschoi96/vectorsearch_image_embeddings_databricks
1. ボリュームにファイルをインポート
Unity Catalogのボリュームは、あなたのマルチモーダルデータに対してスケーラブルで効率的なストレージソリューションを提供します。画像、テキスト文書、音声ファイル、その他のメディアタイムを簡単にボリュームにインポートできるので、処理や分析でアクセスできるようになります。これらはエンベディング空間を共有するので、完全に異なるファイルタイプだったとしても、それらに対して類似検索を行うことができます!
この例では、適切な画像を特定するためにテキストの入力のみをどのように使うのかをデモンストレーションできるように、追加のテキストなしに生の画像のみをインポートします。これによって、より適切なコンテンツを特定するために画像のコンテンツを活用できるようになります。
dest_volume = f"/Volumes/{catalog_name}/{schema_name}/{volume_name}"
def download_file(url, destination):
response = requests.get(url, stream=True)
total_size = int(response.headers.get('content-length', 0))
block_size = 1024
with open(destination, 'wb') as f, tqdm(
total=total_size, unit='B', unit_scale=True) as pbar:
for data in response.iter_content(block_size):
f.write(data)
pbar.update(len(data))
# Download validation images
val_url = "http://images.cocodataset.org/zips/val2017.zip"
val_zip = os.path.join(dest_dir, "val2017.zip")
download_file(val_url, val_zip)
# Extract files
with zipfile.ZipFile(val_zip, 'r') as zip_ref:
zip_ref.extractall(dest_dir)
# Clean up zip files
os.remove(val_zip)
print(f"COCO 2017 validation dataset downloaded to {dest_dir}")
2. BinaryFileフォーマットを用いた画像の格納
DatabricksにおけるBinaryFileフォーマットは、画像のように非テーブルデータの取り扱いに最適化されています。ファイルのメタデータを保持しつつも、効率的にバイナリーデータを格納、処理することができます。このフォーマットはSparkクラスターにおける分散処理を可能にするので、大規模な画像コレクションで特に有用です。
dest_dir = f"/Volumes/{catalog_name}/{schema_name}/{volume_name}/val2017"
image_df = spark.read.format("binaryFile").option("pathGlobFilter", "*.jpg").load(dest_dir)
image_df = image_df.withColumn('path', regexp_replace(col('path'), '^dbfs:', ''))
image_df.write.mode('overwrite').saveAsTable(f"{catalog_name}.{schema_name}.{image_table_name}")
image_df.show(10)
3. Databricksによるエンベディングの生成と格納
Databricksでマルチモーダルエンベディングを作成する際には複数の選択肢があります:
- インメモリ処理: 小規模なデータセットや開発過程では、PyTorchやTensorFlowのようなライブラリを用いてノートブックで直接エンベディングを生成することができます。
- モデルサービング: プロダクションワークロードにおいては、Databricksのモデルサービングがエンドポイントとしてあなたのエンベディングモデルをデプロイするスケーラブルな手段を提供し、高スループットのエンベディング生成を可能にします。
#Download the embedding models from Huggingface
processor = AutoImageProcessor.from_pretrained("nomic-ai/nomic-embed-vision-v1.5")
vision_model = AutoModel.from_pretrained("nomic-ai/nomic-embed-vision-v1.5", trust_remote_code=True)
#Function help generate the embeddings
def generate_image_embedding(image_path):
try:
image = Image.open(image_path)
inputs = processor(images=image, return_tensors="pt")
with torch.no_grad():
outputs = vision_model(**inputs)
embedding = outputs.last_hidden_state
normalized = F.normalize(embedding[:, 0], p=2, dim=1)
return normalized
except Exception as e:
print(f"Error processing {image_path}: {e}")
return None
# We don't need to run all 5000 for this demo. Coco has tens of thousands of images but this demo would take ages to run if we tried to do all of them
image_df = spark.table(f"{catalog_name}.{schema_name}.{image_table_name}").limit(500)
# Collect paths to process - if dataset is large, consider using more efficient approaches
image_paths = image_df.select("path").collect()
path_list = [row["path"] for row in image_paths]
image_data = []
#Production Tip: Serve the embedding model instead and use AI Query for batch infernece. This is not efficient if there are thousands of images
for i, image_path in enumerate(path_list):
image_id = i
embedding = generate_image_embedding(image_path)
if embedding is not None:
flatten_embedding = embedding.flatten()
image_data.append({
"image_id": image_id,
"filepath": image_path,
"embedding": flatten_embedding,
})
print(f"Generated embeddings for {len(image_data)} images")
# Create a DataFrame with image data
image_df_new = pd.DataFrame([{
"image_id": item["image_id"],
"filepath": item["filepath"],
"embedding": item["embedding"].tolist()
} for item in image_data])
# Convert pandas DataFrame to Spark DataFrame
spark_df = spark.createDataFrame(image_df_new)
# Save the DataFrame as a Delta table
delta_table_path = f"{catalog_name}.{schema_name}.{embedding_table_name}"
spark_df.write.format("delta").mode("overwrite").saveAsTable(delta_table_path)
ジョブ/ワークフローによるエンベディング更新の自動化
データが増加すると、あなたのエンベディングを最新の状態に保ちたいと考えるかもしれません。Databricksジョブとワークフローによって、あなたのソースデータの変更を検知し、対応するエンベディングを更新する自動パイプラインを作成することができます。この自動化によって、あなたの検索インデックスは手動での介入なしに最新の状態に保たれます。
Databricks Vector Searchは、ベクトルインデックスとソースデータを自動で同期するDelta Sync APIを提供しています。ソースデータが追加、更新、削除されると、対応するベクトルインデックスは一致するように自動で更新されます。内部では、追加の作業なしにベストなパフォーマンスとスループットを提供するために、Vector Searchは障害の管理、リトライの対応、バッチサイズの最適化を行います。あなたのエージェントは最新のデータで最新の状態にあります!
4. Vector Searchによる類似検索の有効化
エンベディングが生成されると、DatabricksのVector Searchによって効率的な類似検索のためのベクトルインデックスの作成、クエリーが可能となります。
ベクトルインデックスの作成はシンプルです:
- UI、Python SDK、REST APIを用いてVector Searchエンドポイントを作成します
- エンベディングを作成するためにDeltaテーブルのカラムを選択し、モデルサービングエンドポイントを選択します
- あなたの検索クエリーに適したベクトルを特定するためにクエリーします
# Create a Vector Search Index
vs_client = VectorSearchClient()
# Define the endpoint configuration
endpoint_name = vector_search_endpoint_name
delta_table_path = f"{catalog_name}.{schema_name}.{embedding_table_name}"
delta_index_name = f"{catalog_name}.{schema_name}.{index_name}"
vs_client.create_delta_sync_index_and_wait(
index_name=delta_index_name,
endpoint_name=endpoint_name,
pipeline_type="TRIGGERED",
primary_key="image_id",
embedding_vector_column="embedding",
source_table_name=delta_table_path,
embedding_dimension=len(image_data[0]["embedding"])
)
print(f"Vector search index '{index_name}' created on endpoint '{endpoint_name}'")
5. クエリーを変換するためのテキストエンベディングモデルのセットアップ
思い出したかもしれませんが、クエリーのエンベディングはビジョンモデルの出力と同じn空間に存在するようにしなくてはなりません。このステップなしには、画像とテキストを比較する手段が存在しません。このため、クエリーを受け取ったら、Nomicのビジョンエンベディングモデルと同じエンベディング空間のエンベディングを作成するNomicのテキストエンベディングモデルを通じて、テキストをテキストエンベディングに変換する必要があります。
# Load the text model and tokenizer
text_model = AutoModel.from_pretrained('nomic-ai/nomic-embed-text-v1.5', trust_remote_code=True)
text_model.eval()
text_tokenizer = AutoTokenizer.from_pretrained("nomic-ai/nomic-embed-text-v1.5")
#Functions to help with the embedding generation
def mean_pooling(model_output, attention_mask):
token_embeddings = model_output[0]
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)
#Generate Text Embeddings
def generate_text_embedding(query_text):
encoded_input = text_tokenizer(question, padding=True, truncation=True, return_tensors='pt')
with torch.no_grad():
model_output = text_model(**encoded_input)
text_embeddings = mean_pooling(model_output, encoded_input['attention_mask'])
text_embeddings = F.layer_norm(text_embeddings, normalized_shape=(text_embeddings.shape[1],))
text_embeddings = F.normalize(text_embeddings, p=2, dim=1)
text_flatten_embedding = text_embeddings.flatten()
return text_flatten_embedding.tolist()
6. 試しましょう!
これで、画像を検索できるベクトルサーチインデックスが動作しました。
#The search Function
def search_images_by_text(query_text, top_k=5):
query_embedding = generate_text_embedding(query_text)
index = vs_client.get_index(endpoint_name=vector_search_endpoint_name, index_name=delta_index_name)
results = index.similarity_search(num_results=5, columns=["image_id", "filepath"], query_vector=query_embedding)
return results
question = 'search_query: animals that are cute to cuddle with'
search_results = search_images_by_text(question)
#Reconstruct the Images with Matplotlib
#grab the file paths from the vector search results
file_paths = [path[1] for path in search_results['result']['data_array']]
# Display a few sample images
fig, axes = plt.subplots(1, len(file_paths), figsize=(15, 5))
for i, path in enumerate(file_paths):
img = Image.open(path)
axes[i].imshow(img)
axes[i].set_title(f"Image {i+1}")
axes[i].axis('off')
plt.tight_layout()
クエリー「抱きしめたいほど可愛い動物」においては、以下のような画像を見ることになるでしょう:
まとめ
マルチモーダルエンベディングによって、明示的なタグやメタデータに依存することなしに、自然言語を用いて画像を検索するなど、異なるデータタイプ横断でのシームレスな検索が可能となります。
これは、テキストや追加のコンテキストが欠けており、類似度計算でテキストのメタデータとマッチさせることが困難な場合があるいくつかの画像を含むPDFにおいては特に有用です。同じエンベディング空間でエンベディングを作成することで、画像とテキストの両方をRAGアプリケーションの重要な文脈で買うちょうすることができます。
あなたのRAGユースケースをスーパーチャージするために、Databrikcsにおけるマルチモーダルエンベディングの活用方法に関して追加の質問がある場合には、あなたのDatabricksアカウントチームにお問い合わせください!