多模态 AI 实验
从 GitHub 前沿仓库提炼的多模态实验:CLIP 式图文对齐思想、向量检索、OCR 全流程,属于未完成但有研究价值的代码。
项目简介
多模态(Multimodal)指模型同时理解 文本、图像、音频 等信息。本页围绕“图文对齐”给出三个最小实验:特征提取、跨模态检索、OCR 流水线,帮助理解多模态模型的基本数据流。
代码示例:图文特征提取(CLIP 思路)
py
from PIL import Image
import torch
# 需 pip install open_clip_torch
import open_clip
model, _, preprocess = open_clip.create_model_and_transforms(
"ViT-B-32", pretrained="laion2b_s34b_b79k"
)
tokenizer = open_clip.get_tokenizer("ViT-B-32")
def encode_text(texts: list[str]) -> torch.Tensor:
tokens = tokenizer(texts)
with torch.no_grad():
return model.encode_text(tokens)
def encode_image(path: str) -> torch.Tensor:
img = preprocess(Image.open(path)).unsqueeze(0)
with torch.no_grad():
return model.encode_image(img)
# 计算余弦相似度判断“图文是否匹配”
t = encode_text(["一只猫", "一片海"])
i = encode_image("cat.jpg")
print((i @ t.T).softmax(dim=-1))代码示例:跨模态向量检索
py
import numpy as np
# 用 FAISS 建索引:把图片特征存库,用文本特征查询
# pip install faiss-cpu
import faiss
def build_index(vectors: list[np.ndarray]):
d = vectors[0].shape[0]
index = faiss.IndexFlatIP(d) # 内积 = 余弦(归一化后)
mat = np.vstack(vectors).astype("float32")
faiss.normalize_L2(mat)
index.add(mat)
return index
def query(index, text_vec: np.ndarray, k: int = 5):
q = text_vec.astype("float32").reshape(1, -1)
faiss.normalize_L2(q)
scores, idx = index.search(q, k)
return idx[0], scores[0]代码示例:OCR 全流程
py
# pip install paddleocr paddlepaddle
from paddleocr import PaddleOCR
ocr = PaddleOCR(use_angle_cls=True, lang="ch")
def ocr_image(path: str):
result = ocr.ocr(path, cls=True)
lines = []
for line in result:
for item in line:
text = item[1][0]
conf = item[1][1]
lines.append((text, round(conf, 3)))
return lines
# 输出后接正则清洗 + 结构化入库,即为完整文档数字化流水线待完成方向
- 接入真实图文数据集做 zero-shot 分类实验。
- 把 OCR 结果接入命名实体识别(NER),构建知识库。
- 尝试视频帧采样 + 图文对齐做视频检索。
- 评估不同骨干(ViT / ConvNeXt)在中文场景的表现差异。
操作步骤
pip install open_clip_torch faiss-cpu pillow安装依赖。- 准备一张猫的图片运行图文相似度示例,观察匹配分数。
- 把多张图片建索引,再用文字查询返回 Top-K,即完成检索闭环。
完整可运行工程
可直接运行零第三方依赖文本→图像检索
运行环境:Python 3.8+,仅标准库(示例图片用 P3 文本 PPM,记事本即可核对像素值)。先执行 python retrieval.py --make-samples ./samples 生成 4 张示例图;再执行 python retrieval.py ./samples "红色 明亮" 体验跨模态检索;--self-test 会校验语义对齐(同义图文相似度 > 0.98、异色 < 0.3)。
github-multimodal/
├── retrieval.py P3 解析 + 图像/文本双编码器 + 余弦检索 + 自测
├── requirements.txt 依赖清单(核心零依赖)
└── README.md 运行步骤、与 CLIP 的对应关系与替换指引
retrieval.py
检索主脚本:把图像与文本编码到同一 4 维向量空间,用余弦相似度取 top-k。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""retrieval.py —— 跨模态检索最小实现(文本 ↔ 图像,纯标准库)。
思路与 CLIP 一致:把两种模态编码到同一个向量空间,再用余弦相似度排序取 top-k。
本工程的编码器极简,但算法链路完整且真能跑对:
图像 -> [R, G, B, 亮度](通道均值归一化到 0~1)
文本 -> 颜色词 / 明暗词映射到同一 4 维语义空间
换成真实模型时,只需替换 encode_image() / encode_text() 两个函数(见 README)。
用法:
python retrieval.py --make-samples ./samples # 生成 4 张示例图片(P3 PPM 文本格式)
python retrieval.py ./samples "红色 明亮" # 检索最匹配的图片
python retrieval.py ./samples "蓝色" --topk 2
python retrieval.py --self-test # 零配置自测
图片格式:零依赖读取 P3 文本 PPM;若安装了 Pillow,可自行扩展读取 png / jpg。
"""
import argparse
import math
import os
import sys
import tempfile
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
COLORS = {
'红': (1.0, 0.0, 0.0), 'red': (1.0, 0.0, 0.0),
'绿': (0.0, 1.0, 0.0), 'green': (0.0, 1.0, 0.0),
'蓝': (0.0, 0.0, 1.0), 'blue': (0.0, 0.0, 1.0),
'黄': (1.0, 1.0, 0.0), 'yellow': (1.0, 1.0, 0.0),
'紫': (1.0, 0.0, 1.0), 'purple': (1.0, 0.0, 1.0),
'青': (0.0, 1.0, 1.0), 'cyan': (0.0, 1.0, 1.0),
'白': (1.0, 1.0, 1.0), 'white': (1.0, 1.0, 1.0),
'黑': (0.0, 0.0, 0.0), 'black': (0.0, 0.0, 0.0),
'灰': (0.5, 0.5, 0.5), 'gray': (0.5, 0.5, 0.5),
'橙': (1.0, 0.5, 0.0), 'orange': (1.0, 0.5, 0.0),
}
BRIGHT_WORDS = ('明亮', '亮', 'bright', 'light')
DARK_WORDS = ('暗', 'dark', 'dim')
def read_ppm(path):
"""读取 P3 文本格式 PPM,返回 (像素列表 [(r,g,b), ...], 宽, 高, 最大值)。"""
with open(path, encoding='utf-8') as fh:
raw = fh.read()
tokens = []
for line in raw.splitlines():
tokens.extend(line.split('#')[0].split())
assert tokens and tokens[0].upper() == 'P3', '仅支持 P3 文本格式 PPM:%s' % path
width, height, maximum = int(tokens[1]), int(tokens[2]), int(tokens[3])
nums = [int(x) for x in tokens[4:4 + width * height * 3]]
pixels = [(nums[i], nums[i + 1], nums[i + 2]) for i in range(0, len(nums) - 2, 3)]
return pixels, width, height, maximum
def normalize(vec):
norm = math.sqrt(sum(x * x for x in vec))
return [x / norm for x in vec] if norm else list(vec)
def encode_image(path):
"""图像编码器:三通道均值 + 平均亮度,输出 4 维单位向量。"""
pixels, _w, _h, maximum = read_ppm(path)
n = len(pixels) or 1
r = sum(p[0] for p in pixels) / n / maximum
g = sum(p[1] for p in pixels) / n / maximum
b = sum(p[2] for p in pixels) / n / maximum
luma = (r + g + b) / 3.0
return normalize([r, g, b, luma])
def encode_text(text):
"""文本编码器:颜色词映射到 RGB 三通道,明暗词映射到亮度维。"""
vec = [0.0, 0.0, 0.0, 0.5]
low = text.lower()
hits = 0
for word, rgb in COLORS.items():
if word in low:
for i in range(3):
vec[i] += rgb[i]
hits += 1
if any(w in low for w in BRIGHT_WORDS):
vec[3] = 1.0
elif any(w in low for w in DARK_WORDS):
vec[3] = 0.15
if hits == 0:
vec[:3] = [0.34, 0.34, 0.34] # 认不出颜色词时退化为中性灰,避免零向量
return normalize(vec)
def cosine(a, b):
return sum(x * y for x, y in zip(a, b))
def index_folder(folder):
"""把目录里所有可解析图片编码进内存索引(真实系统里应替换为向量库)。"""
index = []
for name in sorted(os.listdir(folder)):
path = os.path.join(folder, name)
if not os.path.isfile(path):
continue
try:
index.append((name, path, encode_image(path)))
except Exception:
continue # 非 PPM / 损坏文件直接跳过
return index
def search(folder, query, topk=3):
qv = encode_text(query)
scored = [(cosine(qv, vec), name, path) for name, path, vec in index_folder(folder)]
scored.sort(key=lambda x: -x[0])
return scored[:topk]
def write_sample(path, rgb, size=8, maximum=255):
lines = ['P3', '%d %d' % (size, size), str(maximum)]
lines += ['%d %d %d' % rgb] * (size * size)
with open(path, 'w', encoding='utf-8', newline='\n') as fh:
fh.write('\n'.join(lines) + '\n')
SAMPLES = (('red.ppm', (255, 0, 0)), ('green.ppm', (0, 255, 0)),
('blue.ppm', (0, 0, 255)), ('white.ppm', (255, 255, 255)))
def make_samples(folder):
os.makedirs(folder, exist_ok=True)
paths = []
for name, rgb in SAMPLES:
p = os.path.join(folder, name)
write_sample(p, rgb)
paths.append(p)
return paths
def selftest():
tmp = tempfile.mkdtemp(prefix='multimodal-selftest-')
make_samples(tmp)
for query, expect in (('红色', 'red.ppm'), ('绿色', 'green.ppm'),
('蓝色', 'blue.ppm'), ('白色 明亮', 'white.ppm')):
hits = search(tmp, query, topk=1)
assert hits and hits[0][1] == expect, (query, hits)
top2 = search(tmp, '红', topk=2)
assert len(top2) == 2 and top2[0][0] > top2[1][0], top2
score = cosine(encode_text('红'), encode_image(os.path.join(tmp, 'red.ppm')))
assert score > 0.98, '同义图文相似度应接近 1:%s' % score
assert cosine(encode_text('红'), encode_image(os.path.join(tmp, 'green.ppm'))) < 0.3
print('selftest OK:P3 解析 / 双模态编码 / 余弦排序 / top-k / 语义对齐 全部通过')
def main():
ap = argparse.ArgumentParser(description='跨模态(文本 -> 图像)检索最小实现')
ap.add_argument('folder', nargs='?', help='图片目录(P3 文本 PPM)')
ap.add_argument('query', nargs='?', help='查询文本,如 "红色 明亮"')
ap.add_argument('--topk', type=int, default=3, help='返回条数')
ap.add_argument('--make-samples', metavar='DIR', help='生成 4 张示例图片到指定目录')
ap.add_argument('--self-test', action='store_true', help='零配置自测')
a = ap.parse_args()
if a.self_test:
selftest()
return
if a.make_samples:
for p in make_samples(a.make_samples):
print('已生成:', p)
return
if not (a.folder and a.query):
ap.error('请提供 目录 与 查询文本,或使用 --self-test')
for score, name, path in search(a.folder, a.query, a.topk):
print('%.4f %s' % (score, name))
if __name__ == '__main__':
main()requirements.txt
依赖清单:核心仅标准库;Pillow / numpy / open_clip / chromadb 为可选增强。
text
# github-multimodal 工程依赖清单
# 核心只用 Python 标准库(math / os / argparse),零安装即可运行:
# python retrieval.py --self-test
# 要求 Python 3.8+。
# 示例图片使用 P3 文本格式 PPM,纯文本、无需 Pillow 即可读写。
#
# 可选增强(把示例编码器换成真实模型时再装):
Pillow>=10.0.0 # 直接读 png / jpg / webp,替换 read_ppm
numpy>=1.26.0 # 相似度计算换成矩阵运算,支持批量检索
open-clip-torch>=2.24.0 # 真实的 CLIP 图文双塔模型
chromadb>=0.4.22 # 换成本地向量库,支撑十万级图片检索
# 安装:pip install -r requirements.txtREADME.md
运行步骤、与 CLIP 的逐项对应关系、需先改的配置项表。
markdown
# github-multimodal · 跨模态图文检索(可运行工程)
用「文本找图片」把多模态检索的完整链路跑通:**双模态编码 → 统一向量空间 → 余弦相似度 → top-k**。
和 CLIP 的差别只在编码器强弱:本工程用颜色/明暗语义做编码,零依赖、可离线复现;换成 CLIP 即为生产方案。
## 目录结构
```
github-multimodal/
├── retrieval.py # P3 PPM 解析 + 图像/文本编码 + 余弦检索 + 自测
├── requirements.txt # 依赖清单(核心零依赖)
└── README.md
```
## 运行环境
- Python 3.8+;核心零第三方依赖。
- 示例图片为 P3 文本 PPM(用记事本就能打开核对像素值),不需要任何图像库。
## 快速开始
```bash
# 1) 生成 4 张示例图片(红/绿/蓝/白)
python retrieval.py --make-samples ./samples
# 2) 用文本检索图片
python retrieval.py ./samples "红色"
python retrieval.py ./samples "蓝" --topk 2
python retrieval.py ./samples "白色 明亮"
# 3) 零配置自测(含语义对齐断言)
python retrieval.py --self-test
```
## 需先改的配置项
| 配置项 | 位置 | 说明 |
|---|---|---|
| 图像编码器 | `retrieval.py` → `encode_image()` | 现为 RGB 均值;换 CLIP 时改成模型推理,输出 512 维向量 |
| 文本编码器 | `retrieval.py` → `encode_text()` | 现为颜色/明暗词典;换 CLIP 时改成同一模型的文本塔 |
| 颜色词表 | `retrieval.py` → `COLORS` | 按你的业务补充中英文词条 |
| 图片格式 | `retrieval.py` → `read_ppm()` | 只有 P3 PPM 零依赖;要读 jpg/png 请装 Pillow 并替换该函数 |
| 向量库 | `retrieval.py` → `index_folder()` | 图片量大时替换为 Chroma / FAISS |
## 与 CLIP 的对应关系
| 本工程 | CLIP 生产方案 |
|---|---|
| `encode_image()` 取通道均值 | 图像塔(ViT / ResNet)输出 512 维 |
| `encode_text()` 词典映射 | 文本塔(Transformer)输出 512 维 |
| `cosine()` + 排序 | 完全相同,可换 FAISS 加速 |
| `index_folder()` 内存索引 | 向量数据库持久化 |需先改的配置项(4 处):
encode_image() / encode_text():换成 CLIP 双塔推理即为生产方案(余弦检索部分无需改动)- 图片格式:默认只读 P3 PPM;要读 jpg/png 请装 Pillow 并替换
read_ppm() COLORS词典:按业务补充中英文颜色/属性词- 图片量大时把
index_folder()换成 Chroma / FAISS 持久化向量库
来源参考
实现与部署所依据的官方文档 / 仓库(均可直接访问):