You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用LangChain FAISS封装器与CLIP模型时余弦相似度得分偏低问题

多模态CLIP+LangChain FAISS向量存储:提升余弦相似度得分的优化方案

问题背景

我基于LangChain的FAISS向量存储封装器和CLIP多模态模型搭建图文检索系统,由于LangChain原生FAISS仅支持文本嵌入模型,因此实现了自定义的CLIPEmbeddings类(继承并实现Embeddings抽象类的方法)。目前系统能通过文本查询匹配到正确的图片,但余弦相似度得分偏低。

当前已采用的配置:

  • 使用FAISS的IndexFlatIP索引
  • 开启LangChain FAISS封装器的normalize_L2=True(通过归一化向量+内积计算余弦相似度)
  • 测试三种池化策略后,选择文本与图片嵌入均用CLS Token的方案(得分最优,但仍有提升空间)

当前FAISS初始化代码

print("Creating new FAISS index...")
index = faiss.IndexFlatIP(DIM)
return FAISS(
    embedding_function=clip_embeddings,
    index=index,
    docstore=InMemoryDocstore({}),
    index_to_docstore_id={},
    normalize_L2 = True,  # cosine similarity via normalized vectors
)

CLS Token池化方案下的测试得分

{
"query": "A sunrise or sunset in the sky with orange shades",
"k": 3
}

similarity scores from vectorstore.similarity_search_with_score : [np.float32(0.227), np.float32(0.125), np.float32(0.104)]
Max Matching score : 0.2272440493106842
------------------------------------------------------------------------------

{
  "query": "Beautiful dog with white fur and staring black eyes",
  "k": 3
}

similarity scores from vectorstore.similarity_search_with_score : [np.float32(0.249), np.float32(0.1), np.float32(0.066)]
Max Matching score : 0.24914392828941345
------------------------------------------------------------------------------

{
  "query": " A black car going on an empty road with trees and windmill in the background ",
  "k": 3
}

similarity scores from vectorstore.similarity_search_with_score : [np.float32(0.184), np.float32(0.133), np.float32(0.054)]
Max Matching score : 0.1843710094690323
------------------------------------------------------------------------------

自定义CLIPEmbeddings代码片段

# Custom CLIPEmbeddings class implementing the methods in abstract class Embeddings
# smaller model - openai/clip-vit-base-patch32
class CLIPEmbeddings(Embeddings):
    def __init__(self, device="cpu"):
        self.model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14").to(device)
        self.tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
        self.processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
        self.device = device

    # mean pooled output
    def get_image_embedding(self, image):
      inputs = self.processor(images=image, return_tensors="pt").to(self.device)
      with torch.no_grad():
        image_features = self.model.get_image_features(**inputs)

      pooled = image_features.last_hidden_state.mean(dim=1)
      embedding = self.model.visual_projection(pooled)

      return embedding.cpu().tolist()

    # CLS Token
    def get_text_embedding(self, text: str):
        inputs = self.tokenizer([text], padding=True, return_tensors="pt").to(self.device)
        with torch.no_grad():
          text_features = self.model.get_text_features(**inputs)

        embedding = text_features.pooler_output
        return embedding.cpu().tolist()


    def embed_documents(self, texts: list[str]) -> list[list[float]]:
      embeddings: list[list[float]] = []
      for text in texts:
        if text.startswith("img_") and text.endswith(".jpg"):
          filepath = os.path.join(IMAGE_DIR, text)
          image = Image.open(filepath).convert("RGB")
          embedding = self.get_image_embedding(image)
        else:
          
          embedding = self.get_text_embedding(text)
        embeddings.extend(embedding)

      return embeddings

    def embed_query(self, text):
      embedding = self.embed_documents([text])[0]
      return embedding

优化方向建议

1. 修正CLIP特征提取逻辑(核心优化点)

CLIP原生的get_image_features和get_text_features方法已经输出了经过视觉/文本投影层+L2归一化的最终特征,不需要额外做池化和投影操作。当前代码中对image_features.last_hidden_state做mean pooling再调用visual_projection,以及取text_features.pooler_output的操作,破坏了CLIP预训练时的图文对齐逻辑,是得分偏低的主要原因。

修正后的特征提取逻辑示例:

def get_image_embedding(self, image):
    inputs = self.processor(images=image, return_tensors="pt").to(self.device)
    with torch.no_grad():
        # get_image_features直接返回归一化后的最终特征
        image_features = self.model.get_image_features(**inputs)
    return image_features.cpu().tolist()

def get_text_embedding(self, text: str):
    inputs = self.tokenizer([text], padding=True, return_tensors="pt").to(self.device)
    with torch.no_grad():
        # get_text_features直接返回归一化后的最终特征
        text_features = self.model.get_text_features(**inputs)
    return text_features.cpu().tolist()

2. 确保特征归一化一致性

虽然FAISS设置了normalize_L2=True,但建议在特征提取后手动做L2归一化,避免中间步骤的数值误差:

import torch.nn.functional as F

# 在get_image_embedding和get_text_embedding中添加归一化
embedding = F.normalize(embedding, p=2, dim=1)

3. 模型与数据优化

  • 尝试更大的CLIP模型:比如openai/clip-vit-giant-patch14,更大的模型通常能提取更精细的特征,提升匹配得分
  • 微调CLIP模型:如果有特定领域的图文数据集,可以在预训练CLIP基础上微调,进一步优化图文对齐效果
  • 优化查询文本:使用更贴合CLIP训练语料的表述(简洁、具象),避免冗余修饰;也可以对同一查询生成多个变体,取嵌入均值作为查询向量,提升鲁棒性

4. 检查基础配置

  • 确认FAISS的DIM参数与CLIP输出特征维度一致(openai/clip-vit-large-patch14的特征维度为768)
  • 确保图片预处理与CLIP训练时完全一致,不要额外修改图片尺寸、色彩空间等

内容的提问来源于stack exchange,提问作者Aashishkumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 13:17:33