CLIP 视觉向量本地化:在 NAS 上跑通以文搜图的第一天
在手机相册里找照片,最让人头疼的是传统的"分类相册":
有的按地点分,有的按人物分,有的按时间分。但当我们真正想找一张照片时,脑海里闪现的往往是一句极其具体的画面描述:
- "老爸在西湖边喂鸽子"
- "小狗 Token 第一次穿红色雨衣"
- "去年中秋节老妈做的那盘清蒸大闸蟹"
传统的相册元数据检索(基于文件名或 GPS 经纬度)在面对这种自然语言描述时彻底失效。而将全家两万张私密照片全量上传到公有云商业相册进行 AI 分析,又存在无法容忍的隐私泄露隐患。
为了让家里的私有 NAS 拥有本地离线、毫秒级、零云端依赖的"以文搜图"能力,我在 NAS 上正式部署并跑通了中文多模态视觉表征模型Chinese-CLIP。
flowchart TD PhotoLibrary[NAS 20,000 张本地照片] --> VisionEncoder[Chinese-CLIP ViT-B/16 视觉编码器] VisionEncoder --> ImageEmbeddings[(512 维图像特征向量库 / 本地 Qdrant/Chroma)] UserSearch[长辈自然语言查询:\n'小狗穿红色雨衣在草地上'] --> TextEncoder[Chinese-CLIP 文本编码器] TextEncoder --> QueryVec[512 维文本查询向量] QueryVec --> DotProduct[点积与余弦相似度极速矩阵检索] DotProduct --> TopMatches[秒级召回 Top-10 高清匹配照片]CLIP (Contrastive Language-Image Pretraining) 的核心底层原理
CLIP 模型之所以能做到"以文搜图",关键在于它在海量图文对上进行了对比学习(Contrastive Learning):
- 它包含两个平行的编码器:图像编码器(Vision Transformer / ResNet)与文本编码器(BERT / Transformer)。
- 两个编码器将图片和文本分别映射到同一个512 维的共享跨模态向量空间。
- 如果一张图片的内容是"穿红雨衣的狗",那么经过 Vision Encoder 算出来的图像向量 $\vec{v}{img}$,与经过 Text Encoder 算出来的文本向量 $\vec{v}{txt}$,在几何空间中的余弦夹角极小(余弦相似度接近 1.0)。
因此,搜图的过程在底层被简化为了纯粹的向量点积与 KNN 邻近检索,不需要昂贵的生成式大模型在旁边喋喋不休,算力开销极小。
基于 ONNXRuntime 与 Python 的本地部署实现
为了在没有独立 GPU 的低功耗 NAS(如 Intel N100 平台)上流畅运行,我们将 Chinese-CLIP 模型导出为INT8 量化的 ONNX 格式:
import numpy as np import onnxruntime as ort from PIL import Image import torchvision.transforms as transforms from typing import List, Tuple class LocalChineseCLIPSearch: def __init__(self, text_onnx_path: str, vision_onnx_path: str): # 针对 CPU 优化推理线程 opts = ort.SessionOptions() opts.intra_op_num_threads = 4 opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.text_session = ort.InferenceSession(text_onnx_path, opts, providers=['CPUExecutionProvider']) self.vision_session = ort.InferenceSession(vision_onnx_path, opts, providers=['CPUExecutionProvider']) self.transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711]) ]) def encode_image(self, image_path: str) -> np.ndarray: """ 提取单张图片的 512 维归一化视觉向量 (耗时约 45ms) """ img = Image.open(image_path).convert('RGB') tensor = self.transform(img).unsqueeze(0).numpy() inputs = {self.vision_session.get_inputs()[0].name: tensor} outs = self.vision_session.run(None, inputs) vec = outs[0][0] return vec / np.linalg.norm(vec) def encode_text(self, text: str, tokenized_ids: np.ndarray) -> np.ndarray: """ 提取文本查询词的 512 维向量 (耗时约 12ms) """ inputs = { self.text_session.get_inputs()[0].name: tokenized_ids, self.text_session.get_inputs()[1].name: np.ones_like(tokenized_ids) } outs = self.text_session.run(None, inputs) vec = outs[0][0] return vec / np.linalg.norm(vec) def search_top_k(self, query_vec: np.ndarray, index_matrix: np.ndarray, image_paths: List[str], k: int = 5) -> List[Tuple[str, float]]: # 矩阵批量点积计算余弦相似度 scores = np.dot(index_matrix, query_vec) top_k_indices = np.argsort(scores)[::-1][:k] return [(image_paths[idx], float(scores[idx])) for idx in top_k_indices]实测以文搜图表现
在 2000 张家庭照片索引库中进行实测:
- "老妈在厨房包饺子":准确召回去年冬至老妈在厨房擀皮、包芹菜猪肉水饺的 4 张特写照片,相似度 0.88;
- "阳台开满黄色花的盆栽":精准召回老爸春天养的文心兰与菊花,耗时仅18ms。
记忆被瞬间点亮的力量
当老妈坐在客厅沙发上,随口说了一句"看看当年小狗刚抱回家那几天的样子",电视屏幕在半秒钟内直接铺满了 Token 幼犬时期毛茸茸的照片时,全家人都由衷地发出了惊呼。
不需要复杂的操作,不需要费心分类。把最前沿的多模态表征模型带进家庭局域网,让每一个模糊的记忆念头都能秒级重现,这就是技术赋予生活最纯粹的魔法。