CH3_04_multi_milvus
2039 字
10 分钟
CH3_04_multi_milvus
本节演示多模态图文检索:用 Visual BGE 把图像/文本变成向量,存入 Milvus,再做相似度搜索。
下面的两个 cell 主要是为代码在 Google Colab 上运行设计。
# Google Colab: run this cell first to install notebook dependencies.import osimport subprocessimport sys
REPO_URL = "https://github.com/datawhalechina/all-in-rag.git"REPO_DIR = "/content/all-in-rag"VISUAL_BGE_DIR = os.path.join(REPO_DIR, "code", "C3", "visual_bge")C3_DIR = os.path.join(REPO_DIR, "code", "C3")
def run(cmd: list[str]) -> None: print("$", " ".join(cmd)) subprocess.check_call(cmd)
# visual_bge ships with this repo; clone it when running on Colab.if not os.path.isdir(VISUAL_BGE_DIR): run(["git", "clone", "--depth", "1", REPO_URL, REPO_DIR])
# Third-party packages used in this notebook.# Colab already provides torch, torchvision, numpy, requests, and Pillow.# milvus-lite must match pymilvus 2.5.x, otherwise search() fails with# MilvusException: function_score (version mismatch in the local DB backend).run( [ sys.executable, "-m", "pip", "install", "-q", "pymilvus==3.0.0", "milvus-lite==3.0", "opencv-python-headless", "tqdm", "transformers>=4.40.0", "huggingface-hub", "timm", "einops", "ftfy", "regex", ])
# Install the local visual_bge package.run([sys.executable, "-m", "pip", "install", "-q", "-e", VISUAL_BGE_DIR])
# Notebook paths (../../models, ../../data) assume cwd is code/C3.os.chdir(C3_DIR)
print("Dependencies installed.")print("Working directory:", os.getcwd())print("Next: download model weights with `!python download_model.py` if needed.")print("If you previously hit a Milvus search error, restart the runtime, then re-run all cells.")!python download_model.pyimport osfrom tqdm import tqdmfrom glob import globimport torchfrom visual_bge.visual_bge.modeling import Visualized_BGEfrom pymilvus import MilvusClient, FieldSchema, CollectionSchema, DataTypeimport numpy as npimport cv2from PIL import Image下面按「库类别 → 简介 → 在本 notebook 中的用途」简要说明这些 import.
tqdm:进度条工具,主要用于在循环中显示处理进度,这里我们在批量生成图像嵌入并插入 Milvus 时显示进度;cv2:计算机视觉库,提供缩放、裁剪、绘制文字、保存图像等操作,这里我们用来在visualized_results中缩放图像、加边框、写 Query 和排名序号,最后保存图像;PIL:图像读写库,用于打开、转换、保存常见格式图像。这里我们用它的Image.open()方法读取查询图和检索结果,转为 RGB,最后用.show()展示拼接后的检索结果图。
这些库如何协作:
flowchart LR
A[glob + os<br/>收集图像路径] --> B[Visualized_BGE + torch<br/>生成向量]
B --> C[pymilvus<br/>存入 Milvus]
D[查询图 + 文本] --> E[Visualized_BGE<br/>生成查询向量]
E --> C
C --> F[检索 Top-K 图像路径]
F --> G[PIL + cv2 + numpy<br/>可视化结果]
# 1. 初始化设置MODEL_NAME = "BAAI/bge-base-en-v1.5"MODEL_PATH = "../../models/bge/Visualized_base_en_v1.5.pth"DATA_DIR = "../../data/C3"COLLECTION_NAME = "multimodal_demo"# MILVUS_URI = "http://localhost:19530"MILVUS_URI = "./milvus_demo.db"# 2. 定义工具 (编码器和可视化函数)class Encoder: """编码器类,用于将图像和文本编码为向量。""" def __init__(self, model_name: str, model_path: str): self.model = Visualized_BGE(model_name_bge=model_name, model_weight=model_path) self.model.eval()
def encode_query(self, image_path: str, text: str) -> list[float]: with torch.no_grad(): query_emb = self.model.encode(image=image_path, text=text) return query_emb.tolist()[0] # 去掉 batch 维度
def encode_image(self, image_path: str) -> list[float]: with torch.no_grad(): query_emb = self.model.encode(image=image_path) return query_emb.tolist()[0]encode_query:同时传入image和text,返回一个 tensor 对象query_emb,它的形状为[1, 768],即 1 条查询的 768 维图文融合向量,且已经经过了 L2 归一化,可以直接用于余弦相似度搜索。encode_image:只传入image,虽然在底层Visualized_BGE里面它也走多模态通路,但文本部分是空字符串,因此只融合了图像。和encode_query主要用于检索不同,它主要用于入库,也就是说:- 文档侧:仅图像
- 查询侧:图像和文本
这样,用户在搜图的时候就能同时给一张图和一句描述,用 encode_query 得到图文联合查询向量,而不是只能以图搜图。
def visualize_results(query_image_path: str, retrieved_images: list, img_height: int = 300, img_width: int = 300, row_count: int = 3) -> np.ndarray: """从检索到的图像列表创建一个全景图用于可视化。""" panoramic_width = img_width * row_count # 创建两张空白画布 panoramic_height = img_height * row_count panoramic_image = np.full((panoramic_height, panoramic_width, 3), 255, dtype=np.uint8) # 左侧结果区 query_display_area = np.full((panoramic_height, img_width, 3), 255, dtype=np.uint8) # 右侧查询区
# 处理查询图像 query_pil = Image.open(query_image_path).convert("RGB") query_cv = np.array(query_pil)[:, :, ::-1] resized_query = cv2.resize(query_cv, (img_width, img_height)) bordered_query = cv2.copyMakeBorder(resized_query, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=(255, 0, 0)) query_display_area[img_height * (row_count - 1):, :] = cv2.resize(bordered_query, (img_width, img_height)) cv2.putText(query_display_area, "Query", (10, panoramic_height - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
# 处理检索到的图像 for i, img_path in enumerate(retrieved_images): row, col = i // row_count, i % row_count start_row, start_col = row * img_height, col * img_width
retrieved_pil = Image.open(img_path).convert("RGB") retrieved_cv = np.array(retrieved_pil)[:, :, ::-1] resized_retrieved = cv2.resize(retrieved_cv, (img_width - 4, img_height - 4)) bordered_retrieved = cv2.copyMakeBorder(resized_retrieved, 2, 2, 2, 2, cv2.BORDER_CONSTANT, value=(0, 0, 0)) panoramic_image[start_row:start_row + img_height, start_col:start_col + img_width] = bordered_retrieved
# 添加索引号 cv2.putText(panoramic_image, str(i), (start_col + 10, start_row + 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
return np.hstack([query_display_area, panoramic_image])这里所谓的全景图就是我们最终保存的结果图像,即把多张图拼成一张大图,方便一眼对比查询图和检索结果。
最终输出的图像结构类似:
最终输出 = 左右拼接(np.hstack)
┌─────────┬──────────────────────────────┐│ │ 检索结果区 (900×900) ││ Query │ ┌───┬───┬───┐ ││ 区 │ │ 0 │ 1 │ 2 │ 第 0 行 ││(300×900)│ ├───┼───┼───┤ ││ │ │ 3 │ 4 │ │ 第 1 行 ││ │ └───┴───┴───┘ ││ [Query] │ (3×3 网格,本例只填 5 张) │└─────────┴──────────────────────────────┘ 300px 900px (3列×300)总宽 1200px,高 900px# 3. 初始化客户端print("--> 正在初始化编码器和Milvus客户端...")encoder = Encoder(MODEL_NAME, MODEL_PATH)milvus_client = MilvusClient(MILVUS_URI)# 4. 创建 Milvus Collectionprint(f"\n--> 正在创建 Collection '{COLLECTION_NAME}'")if milvus_client.has_collection(COLLECTION_NAME): milvus_client.drop_collection(COLLECTION_NAME) print(f"已删除已存在的 Collection: '{COLLECTION_NAME}'")image_list = glob(os.path.join(DATA_DIR, "dragon", "*.png"))if not image_list: raise FileNotFoundError(f"在 {DATA_DIR}/dragon/ 中未找到任何 .png 图像。")dim = len(encoder.encode_image(image_list[0]))
fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dim), FieldSchema(name="image_path", dtype=DataType.VARCHAR, max_length=512),]# 创建集合 Schemaschema = CollectionSchema(fields, description="多模态图文检索")print("Schema 结构:")print(schema)输出为:
Schema 结构:{'auto_id': True, 'description': '多模态图文检索', 'fields': [ {'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'is_primary': True, 'auto_id': True}, {'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, {'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}} ], 'enable_dynamic_field': False, 'enable_namespace': False }上面的输出详细展示了刚刚创建的 multimodal_demo Collection Schema 的完整结构。其包含了三个核心字段(Field):一个自增的 id 作为主键,一个 768 维的 vector 向量字段用于存储图像嵌入,以及一个 image_path 标量字段来记录原始图片路径。
# 创建集合milvus_client.create_collection(collection_name=COLLECTION_NAME, schema=schema)print(f"成功创建 Collection: '{COLLECTION_NAME}'")print("Collection 结构:")print(milvus_client.describe_collection(collection_name=COLLECTION_NAME))输出为:
成功创建 Collection: 'multimodal_demo'Collection 结构:{'collection_name': 'multimodal_demo', 'auto_id': True, 'num_shards': 1, 'description': '', 'fields': [{'field_id': 0, 'name': 'id', 'description': '', 'type': <DataType.INT64: 5>, 'params': {}, 'auto_id': True, 'is_primary': True}, {'field_id': 0, 'name': 'vector', 'description': '', 'type': <DataType.FLOAT_VECTOR: 101>, 'params': {'dim': 768}}, {'field_id': 0, 'name': 'image_path', 'description': '', 'type': <DataType.VARCHAR: 21>, 'params': {'max_length': 512}}], 'functions': [], 'aliases': [], 'collection_id': 0, 'consistency_level': 0, 'consistency_level_name': 'Strong', 'properties': {}, 'num_partitions': 1, 'enable_dynamic_field': False, 'enable_namespace': False}这里则是 multimodal_demo Collection 的完整结构。
# 5. 准备并插入数据print(f"\n--> 正在向 '{COLLECTION_NAME}' 插入数据")data_to_insert = []for image_path in tqdm(image_list, desc="生成图像嵌入"): vector = encoder.encode_image(image_path) data_to_insert.append({"vector": vector, "image_path": image_path})
if data_to_insert: result = milvus_client.insert(collection_name=COLLECTION_NAME, data=data_to_insert) print(f"成功插入 {result['insert_count']} 条数据。")输出为:
生成图像嵌入: 100%|██████████| 7/7 [00:13<00:00, 1.92s/it]成功插入 7 条数据。# 6. 创建索引print(f"\n--> 正在为 '{COLLECTION_NAME}' 创建索引")index_params = milvus_client.prepare_index_params()index_params.add_index( field_name="vector", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 256})milvus_client.create_index(collection_name=COLLECTION_NAME, index_params=index_params)print("成功为向量字段创建 HNSW 索引。")print("索引详情:")print(milvus_client.describe_index(collection_name=COLLECTION_NAME, index_name="vector"))milvus_client.load_collection(collection_name=COLLECTION_NAME)print("已加载 Collection 到内存中。")输出为:
--> 正在为 'multimodal_demo' 创建索引成功为向量字段创建 HNSW 索引。索引详情:{'index_type': 'HNSW', 'metric_type': 'COSINE', 'field_name': 'vector', 'index_name': 'vector', 'total_rows': 7, 'indexed_rows': 7, 'pending_index_rows': 0, 'state': 'Finished'}已加载 Collection 到内存中。可以看出,索引创建成功,在 vector 字段上成功创建了 HNSW 索引,并使用 COSINE 作为距离度量。M: '16' 和 efConstruction: '256' 是 HNSW 索引的两个关键参数,分别控制着图中每个节点的最大连接数和索引构建时的搜索范围,这些参数直接影响检索的性能和准确性。state: 'Finished' 状态表明索引已成功构建。
# 7. 执行多模态检索print(f"\n--> 正在 '{COLLECTION_NAME}' 中执行检索")query_image_path = os.path.join(DATA_DIR, "dragon", "query.png")query_text = "一条龙"query_vector = encoder.encode_query(image_path=query_image_path, text=query_text)
search_results = milvus_client.search( collection_name=COLLECTION_NAME, data=[query_vector], output_fields=["image_path"], limit=5, search_params={"metric_type": "COSINE", "params": {"ef": 128}})[0]
retrieved_images = []print("检索结果:")for i, hit in enumerate(search_results): print(f" Top {i+1}: ID={hit['id']}, 距离={hit['distance']:.4f}, 路径='{hit['entity']['image_path']}'") retrieved_images.append(hit['entity']['image_path'])输出为:
--> 正在 'multimodal_demo' 中执行检索检索结果: Top 1: ID=6, 距离=0.0534, 路径='../../data/C3/dragon/query.png' Top 2: ID=1, 距离=0.2557, 路径='../../data/C3/dragon/dragon02.png' Top 3: ID=4, 距离=0.3149, 路径='../../data/C3/dragon/dragon06.png' Top 4: ID=2, 距离=0.3951, 路径='../../data/C3/dragon/dragon03.png' Top 5: ID=3, 距离=0.4640, 路径='../../data/C3/dragon/dragon05.png'# 8. 可视化与清理print(f"\n--> 正在可视化结果并清理资源")if not retrieved_images: print("没有检索到任何图像。")else: panoramic_image = visualize_results(query_image_path, retrieved_images) combined_image_path = os.path.join(DATA_DIR, "search_result.png") cv2.imwrite(combined_image_path, panoramic_image) print(f"结果图像已保存到: {combined_image_path}") Image.open(combined_image_path).show()
milvus_client.release_collection(collection_name=COLLECTION_NAME)print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'")milvus_client.drop_collection(COLLECTION_NAME)print(f"已删除 Collection: '{COLLECTION_NAME}'")输出为:
--> 正在可视化结果并清理资源结果图像已保存到: ../../data/C3/search_result.png已从内存中释放 Collection: 'multimodal_demo'已删除 Collection: 'multimodal_demo'文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
CH3_04_multi_milvus
https://datawhalechina.github.io/all-in-rag/ 相关文章 智能推荐
1
CH3_03_llamaindex_vector
all-in-rag 实现对 LlamaIndex 存储数据的加载和相似性搜索
2
CH3_02_langchain_faiss
all-in-rag 使用 LangChain 和 FAISS 完成一个完整的“创建 -> 保存 -> 加载 -> 查询”流程
3
CH3_01_bge_visualized
all-in-rag 常用多模态嵌入模型,以 bge-visualized-m3 为例
4
CH2_02_character_splitter
all-in-rag LangChain 中文本分割器的几种核心策略
5
CH2_01_unstructured_example
all-in-rag 如何直接使用 Unstructured 库的示例
随机文章 随机推荐