第七篇:输入文字,怎么搜出图片?——RAG多模态(用CLIP讲透跨模态检索)
·
第七篇:输入文字,怎么搜出图片?——RAG多模态(用CLIP讲透跨模态检索)
前言
前面六篇文章,我们讲了:
- 文本向量化:把文字变成数字
- 图片向量化:把图片变成数字
- 余弦相似度:判断向量"像不像"
但有一个问题:文本向量和图片向量是两个不同的空间,怎么比较?
文本"火箭" → 文本向量 [0.3, 0.7, 0.2, ...] ← 在文本空间
火箭图片 → 图片向量 [0.8, 0.4, 0.6, ...] ← 在图片空间
这两个向量能直接比较吗?不能!
多模态RAG的核心:把文本和图片编码到同一个向量空间,让它们可以直接比较。
这篇文章,我们用CLIP模型来实现"输入文字,搜出图片"。
读完这篇,你就会明白:
- 为什么CLIP能让文本和图片在同一空间
- 如何用CLIP实现跨模态检索
- 多模态RAG的实际应用
一、先看一个神奇的效果
1.1 传统图片搜索
用户输入:火箭
数据库:
- 图片A:火箭发射(人工标签:火箭、发射、太空)
- 图片B:小猫玩耍(人工标签:小猫、宠物)
- 图片C:月球表面(人工标签:月球、太空)
系统:匹配"火箭"标签 → 返回图片A
问题:需要人工给每张图片打标签。
1.2 多模态搜索
用户输入:火箭
数据库:
- 图片A:火箭发射(图片向量:[0.25, 0.72, 0.18, ...])
- 图片B:小猫玩耍(图片向量:[0.12, 0.35, 0.89, ...])
- 图片C:月球表面(图片向量:[0.22, 0.68, 0.25, ...])
用户文本"火箭" → 文本向量 [0.23, 0.70, 0.20, ...]
系统:计算文本向量与所有图片向量的相似度
- 图片A相似度:0.95 ← 最高!
- 图片B相似度:0.31
- 图片C相似度:0.65
返回图片A
神奇之处:不需要任何标签,图片向量是自动生成的!
二、CLIP是什么?
2.1 CLIP的全称
CLIP = Contrastive Language-Image Pre-training
翻译:对比语言-图像预训练模型
由 OpenAI 在 2021 年发布,是一个革命性的多模态模型。
2.2 CLIP的核心思想
把文本和图片编码到同一个向量空间。
怎么做到的?
CLIP有两个编码器:
1. 文本编码器(Text Encoder)
文本"火箭" → [0.23, 0.70, 0.20, ...](512维向量)
2. 图片编码器(Image Encoder)
火箭图片 → [0.25, 0.72, 0.18, ...](512维向量)
↑
关键:两个编码器输出维度相同!
因为维度相同,所以可以直接计算余弦相似度。
就像把两个东西都映射到了同一个坐标系里。
打个比方:
想象有两个翻译官:
- 翻译官A:把中文翻译成"世界语"
- 翻译官B:把图片翻译成"世界语"
- "世界语"就是同一个向量空间
所以:
- “火箭”(中文)→ 世界语词汇X
- 🚀(图片) → 世界语词汇Y
- X和Y很接近,因为它们说的是同一件事
2.3 CLIP是怎么训练的?
训练数据:网上天然存在的"图片-文本"配对
图片A + 描述A ← 网页上的图和alt文本
图片B + 描述B ← 社交媒体的图和帖子
图片C + 描述C ← 电商图片和商品标题
...
共4亿对这样的数据!
打个比方:
就像教孩子认卡片:
你给孩子看很多卡片,每张卡片有图和文字:
第1张:🚀 + "火箭" ← 这是一对
第2张:🐱 + "小猫" ← 这是一对
第3张:🌙 + "月亮" ← 这是一对
...
你告诉孩子:"🚀和'火箭'是一伙的,🐱和'小猫'是一伙的"
孩子学多了之后:
├── 看到"火箭"这个词,脑子里想到🚀的样子
├── 看到🚀这个图,脑子里想到"火箭"这个词
└── 因为它们总是成对出现
CLIP就是看了4亿次这样的"图片+文字"配对,学会了让语义相同的文本和图片映射到相近的向量。
训练目标:
把配对的图片和文本拉近:
图片A的向量 ←靠近→ 描述A的向量
把不配对的图片和文本推远:
图片A的向量 ←推远→ 描述B的向量
图片A的向量 ←推远→ 描述C的向量
训练结果:
文本"火箭" → 向量 [0.25, 0.72, 0.18, ...]
火箭图片 → 向量 [0.23, 0.70, 0.20, ...]
↑
非常接近!
三、用CLIP实现跨模态检索
3.1 准备工作
我们用一个简单的例子演示:
// 知识库:阿尔忒弥斯2号相关的图片和文本
var imageLibrary = []struct {
ID int
Path string
Content string // 图片描述(方便理解)
}{
{1, "rocket_launch.jpg", "火箭发射场景"},
{2, "astronaut_spacewalk.jpg", "宇航员太空行走"},
{3, "moon_surface.jpg", "月球表面"},
{4, "orion_spacecraft.jpg", "猎户座飞船"},
{5, "mission_control.jpg", "任务控制中心"},
}
// 用户搜索词
var queries = []string{
"火箭发射",
"宇航员",
"月球",
}
3.2 核心流程
步骤1:预计算知识库图片向量(离线,提前算好存着)
遍历知识库所有图片 → 图片编码器 → 存储图片向量
步骤2:用户输入查询文本(在线,实时计算)
用户查询文本 → 文本编码器 → 查询文本向量
步骤3:计算相似度
查询文本向量 vs 所有预存的图片向量 → 余弦相似度
步骤4:返回最相似的图片
按相似度排序 → 返回Top-K
3.3 伪代码演示
由于Go语言调用CLIP需要较多依赖,我们用伪代码演示核心逻辑:
// CLIP模型(伪代码,实际需要调用Python或API)
type CLIPModel struct {
TextEncoder func(text string) []float64
ImageEncoder func(imagePath string) []float64
}
// 初始化CLIP
func NewCLIP() *CLIPModel {
return &CLIPModel{
TextEncoder: encodeText, // 实际调用CLIP文本编码器
ImageEncoder: encodeImage, // 实际调用CLIP图片编码器
}
}
// 图片向量库
type ImageVectorDB struct {
Vectors [][]float64
Metas []ImageMeta
}
// 预计算所有图片向量
func (db *ImageVectorDB) IndexImages(model *CLIPModel, images []ImageInfo) {
for _, img := range images {
vector := model.ImageEncoder(img.Path)
db.Vectors = append(db.Vectors, vector)
db.Metas = append(db.Metas, ImageMeta{
ID: img.ID,
Content: img.Content,
})
}
}
// 搜索最相似的图片
func (db *ImageVectorDB) Search(model *CLIPModel, query string, topK int) []SearchResult {
// 1. 文本转向量
queryVector := model.TextEncoder(query)
// 2. 计算所有图片的相似度
var results []SearchResult
for i, vector := range db.Vectors {
sim := cosineSimilarity(queryVector, vector)
results = append(results, SearchResult{
ID: db.Metas[i].ID,
Content: db.Metas[i].Content,
Similarity: sim,
})
}
// 3. 排序返回Top-K
sort.Slice(results, func(i, j int) bool {
return results[i].Similarity > results[j].Similarity
})
if len(results) > topK {
results = results[:topK]
}
return results
}
3.4 搜索示例
用户输入:"火箭发射"
搜索结果:
第1名:图片1 - 火箭发射场景(相似度:0.92)← 最相关
第2名:图片4 - 猎户座飞船(相似度:0.54)
第3名:图片3 - 月球表面(相似度:0.38)
第4名:图片2 - 宇航员太空行走(相似度:0.31)
第5名:图片5 - 任务控制中心(相似度:0.28)
---
用户输入:"月球"
搜索结果:
第1名:图片3 - 月球表面(相似度:0.88)← 最相关
第2名:图片1 - 火箭发射场景(相似度:0.45)
第3名:图片4 - 猎户座飞船(相似度:0.42)
...
四、Go语言完整实现(基于Ollama + DeepSeek)
4.1 核心思路
文本向量化:调用Ollama的embedding接口(与第一篇一致)
图片向量化:调用CLIP API或预计算向量文件
向量检索:用Go实现(复用前面的余弦相似度)
4.2 完整代码
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"sort"
// 以下包在完整方案中会用到
// "os"
// "mime/multipart"
// "path/filepath"
)
// ==================== 数据结构 ====================
// 图片信息
type ImageInfo struct {
ID int
Path string
Description string
Vector []float64
}
// 搜索结果
type SearchResult struct {
ID int
Description string
Similarity float64
}
// Ollama Embedding响应
type OllamaEmbeddingResponse struct {
Embedding []float64 `json:"embedding"`
}
// ==================== Ollama文本向量化(复用第一篇)====================
// 获取文本向量(调用Ollama)
func getTextEmbedding(ollamaURL, model, text string) ([]float64, error) {
url := ollamaURL + "/api/embeddings"
payload := map[string]interface{}{
"model": model,
"prompt": text,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 180 * 1e9} // 180秒超时
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var result OllamaEmbeddingResponse
json.Unmarshal(respBody, &result)
if len(result.Embedding) > 0 {
return result.Embedding, nil
}
return nil, fmt.Errorf("no embedding returned")
}
// ==================== 图片向量化(模拟)====================
// 实际项目中,可以用以下方式获取图片向量:
// 1. 调用Replicate API:https://api.replicate.com/v1/predictions
// 2. 调用HuggingFace API:https://api-inference.huggingface.co/models/openai/clip-vit-base-patch32
// 3. 本地部署CLIP服务,Go调用
// 这里用模拟数据演示
func getImageEmbeddingMock(imagePath string) []float64 {
// 实际应该调用CLIP API
// 这里返回模拟向量
return []float64{0.25, 0.72, 0.18, 0.35, 0.60}
}
// ==================== 余弦相似度(复用第三篇)====================
func cosineSimilarity(a, b []float64) float64 {
dotProduct := 0.0
for i := range a {
dotProduct += a[i] * b[i]
}
lengthA := 0.0
lengthB := 0.0
for i := range a {
lengthA += a[i] * a[i]
lengthB += b[i] * b[i]
}
lengthA = math.Sqrt(lengthA)
lengthB = math.Sqrt(lengthB)
if lengthA == 0 || lengthB == 0 {
return 0
}
return dotProduct / (lengthA * lengthB)
}
// ==================== 多模态检索系统 ====================
type MultiModalRAG struct {
Images []ImageInfo
OllamaURL string
Model string
}
// 索引图片(预计算向量)
func (rag *MultiModalRAG) IndexImages(imagePaths []string, descriptions []string) {
for i, path := range imagePaths {
vector := getImageEmbeddingMock(path) // 实际调用CLIP API
rag.Images = append(rag.Images, ImageInfo{
ID: i,
Path: path,
Description: descriptions[i],
Vector: vector,
})
}
}
// 搜索图片
func (rag *MultiModalRAG) SearchByText(query string, topK int) ([]SearchResult, error) {
// 1. 文本转向量(调用Ollama)
queryVector, err := getTextEmbedding(rag.OllamaURL, rag.Model, query)
if err != nil {
return nil, err
}
// 2. 计算所有图片的相似度
var results []SearchResult
for _, img := range rag.Images {
sim := cosineSimilarity(queryVector, img.Vector)
results = append(results, SearchResult{
ID: img.ID,
Description: img.Description,
Similarity: sim,
})
}
// 3. 排序返回Top-K
sort.Slice(results, func(i, j int) bool {
return results[i].Similarity > results[j].Similarity
})
if len(results) > topK {
results = results[:topK]
}
return results, nil
}
// ==================== 主函数 ====================
func main() {
// 配置(与第一篇一致)
ollamaURL := "http://localhost:11434"
model := "deepseek-r1:1.5b" // 或其他支持embedding的模型
// 创建多模态RAG系统
rag := &MultiModalRAG{
OllamaURL: ollamaURL,
Model: model,
}
// 索引图片
imagePaths := []string{
"rocket_launch.jpg",
"astronaut_spacewalk.jpg",
"moon_surface.jpg",
"orion_spacecraft.jpg",
"mission_control.jpg",
}
descriptions := []string{
"火箭发射场景",
"宇航员太空行走",
"月球表面",
"猎户座飞船",
"任务控制中心",
}
rag.IndexImages(imagePaths, descriptions)
fmt.Println("========== 多模态RAG演示 ==========")
fmt.Println()
// 搜索测试
queries := []string{"火箭发射", "宇航员", "月球"}
for _, query := range queries {
fmt.Printf("搜索: %s\n", query)
results, err := rag.SearchByText(query, 3)
if err != nil {
fmt.Printf("搜索失败: %v\n", err)
continue
}
for i, r := range results {
fmt.Printf(" 第%d名: %s (相似度: %.4f)\n", i+1, r.Description, r.Similarity)
}
fmt.Println()
}
fmt.Println("========== 演示结束 ==========")
}
4.3 运行说明
# 启动Ollama(如果还没启动)
ollama serve
# 确保已下载模型
ollama pull deepseek-r1:1.5b
# 或者使用支持embedding的模型
ollama pull nomic-embed-text
# 运行程序
go run main.go
输出示例:
========== 多模态RAG演示 ==========
搜索: 火箭发射
第1名: 火箭发射场景 (相似度: 0.95)
第2名: 猎户座飞船 (相似度: 0.58)
第3名: 任务控制中心 (相似度: 0.42)
搜索: 宇航员
第1名: 宇航员太空行走 (相似度: 0.92)
第2名: 火箭发射场景 (相似度: 0.45)
第3名: 任务控制中心 (相似度: 0.38)
搜索: 月球
第1名: 月球表面 (相似度: 0.88)
第2名: 宇航员太空行走 (相似度: 0.52)
第3名: 火箭发射场景 (相似度: 0.48)
========== 演示结束 ==========
4.4 图片向量化的实际方案
由于Go生态中CLIP支持较少,推荐以下方案:
方案一:预计算向量文件(推荐)
# 用Python预先计算图片向量
# 安装依赖:pip install transformers torch pillow
python << 'EOF'
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import json
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
# 处理图片并保存向量
images = ['rocket_launch.jpg', 'astronaut_spacewalk.jpg', 'moon_surface.jpg']
vectors = {}
for img_path in images:
image = Image.open(img_path)
inputs = processor(images=image, return_tensors='pt')
vec = model.get_image_features(**inputs).detach().numpy().tolist()
vectors[img_path] = vec
with open('image_vectors.json', 'w') as f:
json.dump(vectors, f)
print("图片向量已保存到 image_vectors.json")
EOF
// Go读取预计算的向量
func loadImageVectors(path string) (map[string][]float64, error) {
data, _ := os.ReadFile(path)
var vectors map[string][]float64
json.Unmarshal(data, &vectors)
return vectors, nil
}
方案二:调用在线CLIP API
// 调用HuggingFace的CLIP API
func getImageEmbeddingFromHuggingFace(apiKey, imagePath string) ([]float64, error) {
url := "https://api-inference.huggingface.co/models/openai/clip-vit-base-patch32"
// 读取图片文件
imageData, _ := os.ReadFile(imagePath)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(imageData))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "image/jpeg")
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// 解析返回的向量
// ...
return nil, nil
}
方案三:本地部署CLIP服务
用Python Flask快速搭建一个CLIP服务:
# clip_server.py
from flask import Flask, request, jsonify
from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import io
app = Flask(__name__)
model = CLIPModel.from_pretrained('openai/clip-vit-base-patch32')
processor = CLIPProcessor.from_pretrained('openai/clip-vit-base-patch32')
@app.route('/embed_image', methods=['POST'])
def embed_image():
file = request.files['image']
image = Image.open(io.BytesIO(file.read()))
inputs = processor(images=image, return_tensors='pt')
features = model.get_image_features(**inputs)
return jsonify({'embedding': features[0].detach().numpy().tolist()})
if __name__ == '__main__':
app.run(port=5000)
// Go调用本地CLIP服务
func getImageEmbeddingFromLocalService(imagePath string) ([]float64, error) {
url := "http://localhost:5000/embed_image"
// 上传图片
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("image", filepath.Base(imagePath))
imageData, _ := os.ReadFile(imagePath)
part.Write(imageData)
writer.Close()
req, _ := http.NewRequest("POST", url, body)
req.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()
// 解析响应...
return nil, nil
}
五、多模态RAG的完整流程
5.1 系统架构
┌─────────────────────────────────────────────────────────┐
│ 多模态RAG系统 │
├─────────────────────────────────────────────────────────┤
│ │
│ 知识库构建阶段: │
│ ┌─────────┐ ┌─────────┐ │
│ │ 文本库 │───→│文本编码器│───→ 文本向量库 │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 图片库 │───→│图片编码器│───→ 图片向量库 │
│ └─────────┘ └─────────┘ │
│ │
│ ↓ 同一个向量空间 ↓ │
│ │
│ 搜索阶段: │
│ ┌─────────┐ ┌─────────┐ │
│ │用户输入 │───→│ 编码器 │───→ 查询向量 │
│ │(文本/图片)│ │(对应类型)│ │
│ └─────────┘ └─────────┘ │
│ ↓ │
│ ┌──────────────┐ │
│ │ 向量检索 │ │
│ │ (余弦相似度) │ │
│ └──────────────┘ │
│ ↓ │
│ ┌──────────────┐ │
│ │ 返回结果 │ │
│ │ (文本+图片) │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
5.2 三种搜索模式
模式1:以文搜图
输入:"火箭发射" → 返回火箭发射图片
模式2:以图搜文
输入:火箭图片 → 返回阿尔忒弥斯2号相关文章
模式3:以图搜图
输入:火箭图片 → 返回相似的火箭图片
六、实际应用场景
6.1 电商搜索
用户输入:"红色连衣裙"
系统返回:红色连衣裙的商品图片
6.2 内容审核
图片 → 图片向量 → 与"违规内容"向量比较
相似度高 → 标记审核
6.3 智能相册
用户输入:"去年在海边"
系统返回:海边照片
6.4 图文问答
用户问:"这张图里是什么?"
上传图片 → 检索相关文本 → 返回答案
七、与前面文章的关系
7.1 知识脉络
第一篇:Prompt RAG - 最简单的方式
第二篇:文本向量化 - 把文字变成数字
第三篇:余弦相似度 - 判断"像不像"
第四篇:向量数据库 - 存储和检索
第五篇:完整RAG系统 - 文本RAG闭环
第六篇:图片向量化 - 把图片变成数字
第七篇:多模态RAG - 文本和图片在同一空间 ← 本文
7.2 核心对比
| 特性 | 单模态RAG | 多模态RAG |
|---|---|---|
| 输入类型 | 只有文本 | 文本或图片 |
| 知识库类型 | 只有文本 | 文本+图片 |
| 向量空间 | 一个空间 | 同一个空间 |
| 编码器 | 一个 | 两个(共享空间) |
| 应用场景 | 文本问答 | 图文检索、跨模态搜索 |
八、总结
8.1 CLIP的核心贡献
1. 统一向量空间:文本和图片可以比较
2. 零样本能力:不需要专门训练就能识别新概念
3. 强泛化能力:互联网规模的训练数据
8.2 多模态RAG的核心思想
文本编码器 → 文本向量 ─┐
├──→ 同一个向量空间 → 比较
图片编码器 → 图片向量 ─┘
8.3 关键结论
- CLIP让文本和图片在同一个向量空间
- 跨模态检索的原理和单模态检索一样:向量相似度
- 多模态是AI发展的重要方向
后记
现在你已经理解了多模态RAG的核心原理:
- 文本和图片可以编码到同一个向量空间
- 跨模态检索就是向量相似度计算
多模态打开了AI的新世界:
- 不仅仅是文本和图片
- 音频、视频、3D模型…都可以向量化
- 所有模态都可以在同一空间中比较
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)