Initial commit
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
import json
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
from transformers import AutoModel, AutoProcessor
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
from scipy.sparse import csr_matrix
|
||||
from pymilvus import connections, MilvusClient, FieldSchema, CollectionSchema, DataType, Collection, AnnSearchRequest, RRFRanker
|
||||
|
||||
# 1. 初始化设置
|
||||
COLLECTION_NAME = "dragon_siglip_demo"
|
||||
MILVUS_URI = "http://localhost:19530" # 服务器模式
|
||||
DATA_PATH = "../../data/C4/metadata/dragon.json" # 相对路径
|
||||
BATCH_SIZE = 50
|
||||
|
||||
# 2. 自定义SigLIP嵌入函数类
|
||||
class SigLIPEmbeddingFunction:
|
||||
def __init__(self, model_name="google/siglip-base-patch16-256-multilingual", device="cpu"):
|
||||
"""
|
||||
初始化SigLIP嵌入函数
|
||||
Args:
|
||||
model_name: SigLIP模型名称
|
||||
device: 设备类型 ("cpu" 或 "cuda")
|
||||
"""
|
||||
self.model_name = model_name
|
||||
self.device = device
|
||||
|
||||
print(f"--> 正在加载 SigLIP 模型: {model_name}")
|
||||
self.model = AutoModel.from_pretrained(model_name)
|
||||
self.processor = AutoProcessor.from_pretrained(model_name)
|
||||
self.model.to(device)
|
||||
self.model.eval()
|
||||
|
||||
# 初始化TF-IDF作为稀疏向量生成器
|
||||
self.tfidf_vectorizer = TfidfVectorizer(
|
||||
max_features=10000, # 限制词汇表大小以节省空间
|
||||
stop_words='english',
|
||||
ngram_range=(1, 2)
|
||||
)
|
||||
self.tfidf_fitted = False
|
||||
|
||||
# 获取文本编码器的输出维度
|
||||
with torch.no_grad():
|
||||
dummy_text = ["test"]
|
||||
inputs = self.processor(text=dummy_text, padding="max_length", return_tensors="pt")
|
||||
outputs = self.model.text_model(**{k: v.to(device) for k, v in inputs.items() if k != 'pixel_values'})
|
||||
self.dense_dim = outputs.pooler_output.shape[-1]
|
||||
|
||||
print(f"--> SigLIP 模型加载完成。密集向量维度: {self.dense_dim}")
|
||||
|
||||
@property
|
||||
def dim(self):
|
||||
"""返回维度信息,兼容原BGE-M3接口"""
|
||||
return {
|
||||
"dense": self.dense_dim,
|
||||
"sparse": self.tfidf_vectorizer.max_features if self.tfidf_fitted else 10000
|
||||
}
|
||||
|
||||
def fit_sparse(self, docs):
|
||||
"""拟合稀疏向量模型(TF-IDF)"""
|
||||
print("--> 正在拟合 TF-IDF 模型...")
|
||||
self.tfidf_vectorizer.fit(docs)
|
||||
self.tfidf_fitted = True
|
||||
print(f"--> TF-IDF 模型拟合完成。词汇表大小: {len(self.tfidf_vectorizer.vocabulary_)}")
|
||||
|
||||
def encode_text_dense(self, texts):
|
||||
"""使用SigLIP编码文本为密集向量"""
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
|
||||
dense_vectors = []
|
||||
batch_size = 8 # 减小批次大小以节省内存
|
||||
|
||||
with torch.no_grad():
|
||||
for i in range(0, len(texts), batch_size):
|
||||
batch_texts = texts[i:i + batch_size]
|
||||
inputs = self.processor(text=batch_texts, padding="max_length", truncation=True, return_tensors="pt")
|
||||
inputs = {k: v.to(self.device) for k, v in inputs.items() if k != 'pixel_values'}
|
||||
|
||||
outputs = self.model.text_model(**inputs)
|
||||
embeddings = outputs.pooler_output
|
||||
|
||||
# 归一化向量
|
||||
embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=1)
|
||||
dense_vectors.extend(embeddings.cpu().numpy())
|
||||
|
||||
return np.array(dense_vectors)
|
||||
|
||||
def encode_text_sparse(self, texts):
|
||||
"""使用TF-IDF编码文本为稀疏向量"""
|
||||
if not self.tfidf_fitted:
|
||||
raise ValueError("请先调用 fit_sparse() 方法拟合TF-IDF模型")
|
||||
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
|
||||
sparse_matrix = self.tfidf_vectorizer.transform(texts)
|
||||
return sparse_matrix
|
||||
|
||||
def __call__(self, texts):
|
||||
"""主调用方法,返回密集和稀疏向量"""
|
||||
if isinstance(texts, str):
|
||||
texts = [texts]
|
||||
|
||||
# 如果还没有拟合稀疏模型,先拟合
|
||||
if not self.tfidf_fitted:
|
||||
self.fit_sparse(texts)
|
||||
|
||||
dense_vectors = self.encode_text_dense(texts)
|
||||
sparse_vectors = self.encode_text_sparse(texts)
|
||||
|
||||
return {
|
||||
"dense": dense_vectors,
|
||||
"sparse": sparse_vectors
|
||||
}
|
||||
|
||||
# 3. 连接 Milvus 并初始化嵌入模型
|
||||
print(f"--> 正在连接到 Milvus: {MILVUS_URI}")
|
||||
connections.connect(uri=MILVUS_URI)
|
||||
|
||||
print("--> 正在初始化 SigLIP 嵌入模型...")
|
||||
ef = SigLIPEmbeddingFunction(device="cpu") # 如果有GPU可以改为"cuda"
|
||||
|
||||
# 4. 创建 Collection
|
||||
milvus_client = MilvusClient(uri=MILVUS_URI)
|
||||
if milvus_client.has_collection(COLLECTION_NAME):
|
||||
print(f"--> 正在删除已存在的 Collection '{COLLECTION_NAME}'...")
|
||||
milvus_client.drop_collection(COLLECTION_NAME)
|
||||
|
||||
fields = [
|
||||
FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, auto_id=True, max_length=100),
|
||||
FieldSchema(name="img_id", dtype=DataType.VARCHAR, max_length=100),
|
||||
FieldSchema(name="path", dtype=DataType.VARCHAR, max_length=256),
|
||||
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256),
|
||||
FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=4096),
|
||||
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64),
|
||||
FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=128),
|
||||
FieldSchema(name="environment", dtype=DataType.VARCHAR, max_length=64),
|
||||
FieldSchema(name="sparse_vector", dtype=DataType.SPARSE_FLOAT_VECTOR),
|
||||
FieldSchema(name="dense_vector", dtype=DataType.FLOAT_VECTOR, dim=ef.dim["dense"])
|
||||
]
|
||||
|
||||
# 如果集合不存在,则创建它及索引
|
||||
if not milvus_client.has_collection(COLLECTION_NAME):
|
||||
print(f"--> 正在创建 Collection '{COLLECTION_NAME}'...")
|
||||
schema = CollectionSchema(fields, description="使用SigLIP的龙混合检索示例")
|
||||
# 创建集合
|
||||
collection = Collection(name=COLLECTION_NAME, schema=schema, consistency_level="Strong")
|
||||
print("--> Collection 创建成功。")
|
||||
|
||||
# 5. 创建索引
|
||||
print("--> 正在为新集合创建索引...")
|
||||
sparse_index = {"index_type": "SPARSE_INVERTED_INDEX", "metric_type": "IP"}
|
||||
collection.create_index("sparse_vector", sparse_index)
|
||||
print("稀疏向量索引创建成功。")
|
||||
|
||||
dense_index = {"index_type": "AUTOINDEX", "metric_type": "IP"}
|
||||
collection.create_index("dense_vector", dense_index)
|
||||
print("密集向量索引创建成功。")
|
||||
|
||||
collection = Collection(COLLECTION_NAME)
|
||||
|
||||
# 6. 加载数据并插入
|
||||
collection.load()
|
||||
print(f"--> Collection '{COLLECTION_NAME}' 已加载到内存。")
|
||||
|
||||
if collection.is_empty:
|
||||
print(f"--> Collection 为空,开始插入数据...")
|
||||
if not os.path.exists(DATA_PATH):
|
||||
raise FileNotFoundError(f"数据文件未找到: {DATA_PATH}")
|
||||
with open(DATA_PATH, 'r', encoding='utf-8') as f:
|
||||
dataset = json.load(f)
|
||||
|
||||
docs, metadata = [], []
|
||||
for item in dataset:
|
||||
parts = [
|
||||
item.get('title', ''),
|
||||
item.get('description', ''),
|
||||
item.get('location', ''),
|
||||
item.get('environment', ''),
|
||||
# *item.get('combat_details', {}).get('combat_style', []),
|
||||
# *item.get('combat_details', {}).get('abilities_used', []),
|
||||
# item.get('scene_info', {}).get('time_of_day', '')
|
||||
]
|
||||
docs.append(' '.join(filter(None, parts)))
|
||||
metadata.append(item)
|
||||
print(f"--> 数据加载完成,共 {len(docs)} 条。")
|
||||
|
||||
print("--> 正在生成向量嵌入...")
|
||||
embeddings = ef(docs)
|
||||
print("--> 向量生成完成。")
|
||||
|
||||
print("--> 正在分批插入数据...")
|
||||
# 为每个字段准备批量数据
|
||||
img_ids = [doc["img_id"] for doc in metadata]
|
||||
paths = [doc["path"] for doc in metadata]
|
||||
titles = [doc["title"] for doc in metadata]
|
||||
descriptions = [doc["description"] for doc in metadata]
|
||||
categories = [doc["category"] for doc in metadata]
|
||||
locations = [doc["location"] for doc in metadata]
|
||||
environments = [doc["environment"] for doc in metadata]
|
||||
|
||||
# 获取向量 - 注意SigLIP返回的格式与BGE-M3不同
|
||||
sparse_vectors = []
|
||||
dense_vectors = embeddings["dense"].tolist()
|
||||
|
||||
# 将稀疏矩阵转换为Milvus可接受的格式
|
||||
sparse_matrix = embeddings["sparse"]
|
||||
for i in range(sparse_matrix.shape[0]):
|
||||
row = sparse_matrix.getrow(i)
|
||||
# 创建稀疏向量字典格式
|
||||
sparse_dict = {}
|
||||
for j in range(row.nnz):
|
||||
sparse_dict[row.indices[j]] = float(row.data[j])
|
||||
sparse_vectors.append(sparse_dict)
|
||||
|
||||
# 插入数据
|
||||
collection.insert([
|
||||
img_ids,
|
||||
paths,
|
||||
titles,
|
||||
descriptions,
|
||||
categories,
|
||||
locations,
|
||||
environments,
|
||||
sparse_vectors,
|
||||
dense_vectors
|
||||
])
|
||||
|
||||
collection.flush()
|
||||
print(f"--> 数据插入完成,总数: {collection.num_entities}")
|
||||
else:
|
||||
print(f"--> Collection 中已有 {collection.num_entities} 条数据,跳过插入。")
|
||||
|
||||
# 7. 执行搜索
|
||||
search_query = "悬崖上的巨龙"
|
||||
search_filter = 'category in ["western_dragon", "chinese_dragon", "movie_character"]'
|
||||
top_k = 5
|
||||
|
||||
print(f"\n{'='*20} 开始混合搜索 {'='*20}")
|
||||
print(f"查询: '{search_query}'")
|
||||
print(f"过滤器: '{search_filter}'")
|
||||
|
||||
# 生成查询向量
|
||||
query_embeddings = ef([search_query])
|
||||
dense_vec = query_embeddings["dense"][0].tolist()
|
||||
|
||||
# 处理稀疏向量
|
||||
sparse_matrix = query_embeddings["sparse"]
|
||||
sparse_row = sparse_matrix.getrow(0)
|
||||
sparse_dict = {}
|
||||
for j in range(sparse_row.nnz):
|
||||
sparse_dict[sparse_row.indices[j]] = float(sparse_row.data[j])
|
||||
|
||||
# 打印向量信息
|
||||
print("\n=== 向量信息 ===")
|
||||
print(f"密集向量维度: {len(dense_vec)}")
|
||||
print(f"密集向量前5个元素: {dense_vec[:5]}")
|
||||
print(f"密集向量范数: {np.linalg.norm(dense_vec):.4f}")
|
||||
|
||||
print(f"\n稀疏向量维度: {sparse_matrix.shape[1]}")
|
||||
print(f"稀疏向量非零元素数量: {sparse_row.nnz}")
|
||||
print("稀疏向量前5个非零元素:")
|
||||
for i, (idx, val) in enumerate(list(sparse_dict.items())[:5]):
|
||||
print(f" - 索引: {idx}, 值: {val:.4f}")
|
||||
density = (sparse_row.nnz / sparse_matrix.shape[1] * 100)
|
||||
print(f"\n稀疏向量密度: {density:.8f}%")
|
||||
|
||||
# 定义搜索参数
|
||||
search_params = {"metric_type": "IP", "params": {}}
|
||||
|
||||
# 先执行单独的搜索
|
||||
print("\n--- [单独] 密集向量搜索结果 ---")
|
||||
dense_results = collection.search(
|
||||
[dense_vec],
|
||||
anns_field="dense_vector",
|
||||
param=search_params,
|
||||
limit=top_k,
|
||||
expr=search_filter,
|
||||
output_fields=["title", "path", "description", "category", "location", "environment"]
|
||||
)[0]
|
||||
|
||||
for i, hit in enumerate(dense_results):
|
||||
print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})")
|
||||
print(f" 路径: {hit.entity.get('path')}")
|
||||
print(f" 描述: {hit.entity.get('description')[:100]}...")
|
||||
|
||||
print("\n--- [单独] 稀疏向量搜索结果 ---")
|
||||
sparse_results = collection.search(
|
||||
[sparse_dict],
|
||||
anns_field="sparse_vector",
|
||||
param=search_params,
|
||||
limit=top_k,
|
||||
expr=search_filter,
|
||||
output_fields=["title", "path", "description", "category", "location", "environment"]
|
||||
)[0]
|
||||
|
||||
for i, hit in enumerate(sparse_results):
|
||||
print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})")
|
||||
print(f" 路径: {hit.entity.get('path')}")
|
||||
print(f" 描述: {hit.entity.get('description')[:100]}...")
|
||||
|
||||
print("\n--- [混合] 稀疏+密集向量搜索结果 ---")
|
||||
# 创建 RRF 融合器
|
||||
rerank = RRFRanker(k=60)
|
||||
|
||||
# 创建搜索请求
|
||||
dense_req = AnnSearchRequest([dense_vec], "dense_vector", search_params, limit=top_k)
|
||||
sparse_req = AnnSearchRequest([sparse_dict], "sparse_vector", search_params, limit=top_k)
|
||||
|
||||
# 执行混合搜索
|
||||
results = collection.hybrid_search(
|
||||
[sparse_req, dense_req],
|
||||
rerank=rerank,
|
||||
limit=top_k,
|
||||
output_fields=["title", "path", "description", "category", "location", "environment"]
|
||||
)[0]
|
||||
|
||||
# 打印最终结果
|
||||
for i, hit in enumerate(results):
|
||||
print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})")
|
||||
print(f" 路径: {hit.entity.get('path')}")
|
||||
print(f" 描述: {hit.entity.get('description')[:100]}...")
|
||||
|
||||
# 8. 清理资源
|
||||
milvus_client.release_collection(collection_name=COLLECTION_NAME)
|
||||
print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'")
|
||||
milvus_client.drop_collection(COLLECTION_NAME)
|
||||
print(f"已删除 Collection: '{COLLECTION_NAME}'")
|
||||
Reference in New Issue
Block a user