AIGC:仅部分代码
TL;DR
如果你有图片语义检索的需求,目前大多数相册都已支持此功能,而且开源流行的 immich 也可以满足私有化场景图片分类和语义检索的完善功能了。但是如果你想理解向量嵌入等一些 AI 概念,又需要动手来加深理解,同时还想创造一些有价值效果酷炫的小工具的话,一个表情包搜索引擎或许是再合适不过的选择了。
其实很早之前就收藏了一篇与此相关的 HN 热文——我不小心构建了一个表情包搜索引擎,但是当时只是粗略浏览一遍,没动手实践过。最新有些想法心血来潮决定上手试试能做到什么程度。
Step 0 - 开始之前
虽然之前或多或少听过以下一些名词,这里还是枚举简单回顾下:
- 向量(Vector):可以简单理解为一组数字表示,比如 $[x, y]$ 就是一个二维向量,而机器学习中一般会用更高维度的向量来表示多模态数据。
- 向量嵌入(Vector Embeddings):把文本或图像转换成数值向量,从而可高效进行相似性检索。
- 向量数据库(Vector Database):专门存储并检索这些向量的数据库,帮你迅速找出相似项。
- CLIP(Contrastive Language-Image Pre-training):OpenAI 的模型,可把图像和文本同时编码成向量。
而一个图片搜索引擎主要的流程其实很简单:
- 遍历读取所有图片输入模型,输出向量后存储到库或文件。
- 根据用户输入检索向量库,返回对应图片。
Step 1 - 嵌入向量数据
考虑到我需要处理的图片可能包含中英文,同时不想依赖云服务的 API,vibe search 了一下本来决定使用 jina-clip-v2 这个看起来效果比较好的多语言多模态嵌入模型,结果因为 PyTorch 兼容性等问题报错给我劝退,决定先回退到 CLIP 这个轻量流行的模型用。
1 2 3 4
| from pathlib import Path import torch from PIL import Image from transformers import CLIPModel, CLIPProcessor
|
虽然这几个库都大名鼎鼎,但是出于项目本身的学习目的,这里还是记录它们的作用:
- transormers:负责模型的加载和运行,自动下载模型,并将输入向量化。
- torch(aka PyTorch):负责处理计算和模型的存储读取。
- PIL(Pillow):负责图像处理和编辑。
- pathlib:负责目录遍历。
首先添加一些基本参数配置:
1 2 3 4
| IMAGE_DIR = r"C:\Users\Rosin\Pictures" SAVE_PATH = "vectors.pt" MODEL_NAME = "openai/clip-vit-base-patch32" SUPPORTED_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
|
- 图片路径前的
r 用于处理 Windows 路径转义
- 模型先使用 openai 的 clip,该模型在中文输入下能力欠缺,但是后续可以简单替换
transformers 已经提供了标准的 CLIP 接口,直接拿来用就行:
1 2 3 4
| model = CLIPModel.from_pretrained(MODEL_NAME) processor = CLIPProcessor.from_pretrained(MODEL_NAME)
model.eval()
|
接下来可以开始循环遍历图片来创建向量:
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
| image_paths = [ p for p in Path(IMAGE_DIR).rglob("*") if p.suffix.lower() in SUPPORTED_EXTS ] print(f"📂 共找到 {len(image_paths)} 张图片\n")
paths: list[str] = [] vectors: list[torch.Tensor] = []
for i, img_path in enumerate(image_paths, 1): try: img = Image.open(img_path).convert("RGB") except Exception as e: print(f" ⚠️ 跳过: {img_path.name} ({e})") continue
inputs = processor(images=img, return_tensors="pt")
with torch.no_grad(): vec = model.get_image_features(pixel_values=inputs["pixel_values"]) if not isinstance(vec, torch.Tensor): vec = vec.pooler_output vec = vec / vec.norm(dim=-1, keepdim=True)
paths.append(str(img_path)) vectors.append(vec) print(f" [{i}/{len(image_paths)}] {img_path.name}")
|
遍历完成后保存到向量文件 vectors.pt:
1 2 3
| matrix = torch.cat(vectors, dim=0) torch.save({"paths": paths, "vectors": matrix}, SAVE_PATH)
|
完成后可以打印查看向量格式:
1 2 3 4 5 6 7 8 9 10
| print(f" 图片数量: {len(paths)}") print(f" 向量维度: {matrix.shape[1]}") print(f" 矩阵形状: {matrix.shape}")
print("\n🔍 数据预览 (前 3 条):") for i in range(min(3, len(paths))): print(f" {i+1}. 路径: {Path(paths[i]).name}") sample = matrix[i][:5].tolist() print(f" 向量预览: {[round(x, 4) for x in sample]}...")
|
1 2 3 4 5 6 7 8
| 图片数量: 42 向量维度: 512 矩阵形状: torch.Size([42, 512])
🔍 数据预览 (前 3 条): 向量预览: [0.0392, -0.0132, 0.0034, -0.0282, 0.0004]... 向量预览: [0.0088, -0.0059, -0.0319, 0.0002, -0.0078]... 向量预览: [0.0275, 0.0034, -0.0185, -0.0581, 0.0342]...
|
可以注意到每张图片都被转换为了一条 512 维度的向量数据嵌入,而后续如果需要更新这个向量库(添加图片),也可以单独转换插入即可。
Step 2 - 实现图片检索