Elasticsearch:探索 CLIP 替代方案

165 阅读16分钟

作者:来自 Elastic Jeffrey Rengifo 及 Tomás Murúa

分析图像到图像和文本到图像搜索的 CLIP 模型的替代方案。

在本文中,我们将通过一个模拟房地产网站的实际示例介绍 CLIP 多模态模型,探索替代方案,并分析它们的优缺点,该网站允许用户使用图片作为参考来搜索房产。

什么是 CLIP?

CLIP(Contrastive Language–Image Pre-training - 对比语言 - 图像预训练)是由 OpenAI 创建的神经网络,使用图像和文本对进行训练,以解决在文本和图像之间寻找相似性的任务,并对 “零样本” 图像进行分类,因此模型不是使用固定标签进行训练的,而是我们为模型提供未知类别,以便它可以对我们提供的图像进行分类。

CLIP 一直是最先进的模型,你可以在此处阅读有关它的更多文章:

然而,随着时间的推移,出现了更多的替代方案。

在本文中,我们将使用房地产示例介绍 CLIP 的两种替代方案的优缺点。以下是我们在本文中将遵循的步骤的摘要:

基本配置:CLIP 和 Elasticsearch

在我们的示例中,我们将使用 Python 创建一个带有交互式 UI 的小项目。我们将安装一些依赖项,例如 Python 转换器,这将授予我们访问我们将使用的某些模型的权限。

创建一个文件夹 /clip_comparison 并按照此处的安装说明进行操作。完成后,安装 Elasticsearch 的 Python 客户端、Cohere SDK 和 Streamlit:

注意:作为一种选择,我建议使用 Python 虚拟环境 (venv)。如果你不想在计算机上安装所有依赖项,这将非常有用。

pip install elasticsearch==8.15.0 cohere streamlit

Streamlit 是一个开源 Python 框架,可让你使用少量代码轻松获得 UI。

我们还将创建一些文件来保存稍后将使用的指令:

  • app.py:UI 逻辑。
  • /services/elasticsearch.py​​:Elasticsearch 客户端初始化、查询和批量 API 调用以索引文档。
  • /services/models.py:用于生成嵌入的模型实例和方法。
  • index_data.py:用于从本地源索引图像的脚本。
  • /data:我们的数据集目录。

我们的应用程序结构应如下所示:



1.  /clip_comparison
2.    |--app.py
3.    |--index_data.py
4.    |--/data
5.    |--/venv # If you decide to use venv
6.    |--/services
7.          |-- __init__.py
8.          |-- models.py
9.          |-- elasticsearch.py


配置 Elasticsearch

按照以下步骤存储示例图像。然后我们将使用 knn 向量查询搜索它们。

注意:我们也可以存储文本文档,但对于此示例,我们将仅在图像中搜索。

索引映射

访问 Kibana 开发工具(从 Kibana:Management > Dev Tools)以使用这些映射构建数据结构:



1.  [ ]
2.  PUT clip-images
3.  {
4.    "mappings": {
5.      "properties": {
6.        "image_name": {
7.          "type": "text",
8.          "fields": {
9.            "keyword": {
10.              "type": "keyword"
11.            }
12.          }
13.        },
14.        "image_embedding": {
15.          "type": "dense_vector",
16.          "dims": 768,
17.          "index": "true",
18.          "similarity": "cosine"
19.        },
20.        "image_data": {
21.          "type": "binary"
22.        }
23.      }
24.    }
25.  }

27.  PUT embed-images
28.  {
29.    "mappings": {
30.      "properties": {
31.        "image_name": {
32.          "type": "text",
33.          "fields": {
34.            "keyword": {
35.              "type": "keyword"
36.            }
37.          }
38.        },
39.        "image_embedding": {
40.          "type": "dense_vector",
41.          "dims": 1024,
42.          "index": "true",
43.          "similarity": "cosine"
44.        },
45.        "image_data": {
46.          "type": "binary"
47.        }
48.      }
49.    }
50.  }
51.  PUT jina-images
52.  {
53.    "mappings": {
54.      "properties": {
55.        "image_name": {
56.          "type": "text",
57.          "fields": {
58.            "keyword": {
59.              "type": "keyword"
60.            }
61.          }
62.        },
63.        "image_embedding": {
64.          "type": "dense_vector",
65.          "dims": 768,
66.          "index": "true",
67.          "similarity": "cosine"
68.        },
69.        "image_data": {
70.          "type": "binary"
71.        }
72.      }
73.    }
74.  }


字段类型 dense_vector 将存储模型生成的嵌入。字段 binary 将以 base64 格式存储图像。

注意:将图像以二进制形式存储在 Elasticsearch 中不是一个好习惯。我们这样做只是为了这个例子的实际目的。建议使用静态文件存储库。

现在来看看代码。我们需要做的第一件事是使用 cloud id 和 api-key 初始化 Elasticsearch 客户端。在文件 /services/elasticsearch.py​​ 的开头写入以下代码:



1.  [ ]
2.  from elasticsearch import Elasticsearch, exceptions, helpers
3.  ELASTIC_ENDPOINT = "https://your-elastic-endpoint.com:9243"
4.  ELASTIC_API_KEY = "your-elasticsearch-api-key"
5.  # Elasticsearch client
6.  es_client = Elasticsearch(
7.      ELASTIC_ENDPOINT,
8.      api_key=ELASTIC_API_KEY,
9.  )
10.  # index documents using bulk api
11.  def index_images(index_name: str, images_obj_arr: list):

13.      actions = [
14.          {
15.              "_index": index_name,
16.              "_source": {
17.                  "image_data": obj["image_data"],
18.                  "image_name": obj["image_name"],
19.                  "image_embedding": obj["image_embedding"],
20.              },
21.          }
22.          for obj in images_obj_arr
23.      ]
24.      try:
25.          response = helpers.bulk(es_client, actions)
26.          return response
27.      except exceptions.ConnectionError as e:
28.          return e

30.  # knn search
31.  def knn_search(index_name: str, query_vector: list, k: int):
32.      query = {
33.          "size": 4,
34.          "_source": ["image_name", "image_data"],
35.          "query": {
36.              "knn": {
37.                  "field": "image_embedding",
38.                  "query_vector": query_vector,
39.                  "k": k,
40.                  "num_candidates": 100,
41.                  "boost": 10,
42.              }
43.          },
44.      }
45.      try:
46.          response = es_client.search(index=index_name, body=query)
47.          return response
48.      except exceptions.ConnectionError as e:
49.          return e
50.  # match all query
51.  def get_all_query(index_name: str):
52.      query = {
53.          "size": 400,
54.          "source": ["image_name", "image_data"],
55.          "query": {"match_all": {}},
56.      }
57.      try:
58.          return es_client.search(index=index_name, body=query)
59.      except exceptions.ConnectionError as e:
60.          return e


配置模型

要配置模型,请将模型实例及其方法放入此文件中:/services/models.py。

Cohere Embed-3 模型作为 Web 服务工作,因此我们需要一个 API 密钥才能使用它。你可以在此处免费获取一个。试用限制为每分钟 5 次调用,每月 1,000 次调用。

要配置模型并使图像可在 Elasticsearch 中搜索,请按照以下步骤操作:

  • 使用 CLIP 将图像转换为向量
  • 将图像像量存储在 Elasticsearch 中
  • 将我们要与存储的图像进行比较的图像或文本向量化。
  • 运行查询以将上一步的条目与存储的图像进行比较并获取最相似的图像。


1.  [ ]
2.  # /services/models.py
3.  # dependencies
4.  import base64
5.  import io
6.  import cohere
7.  from PIL import Image
8.  from transformers import CLIPModel, CLIPProcessor, AutoModel
9.  COHERE_API_KEY = "your-cohere-api-key"
10.  ## CLIP model call
11.  clip_model = CLIPModel.from_pretrained("openai/clip-vit-large-patch14")
12.  clip_processor = CLIPProcessor.from_pretrained("openai/clip-vit-large-patch14")
13.  # JinaCLip model call
14.  jina_model = AutoModel.from_pretrained("jinaai/jina-clip-v1", trust_remote_code=True)
15.  # Cohere client initialization
16.  co = cohere.ClientV2(COHERE_API_KEY)


配置 CLIP

要配置 CLIP,我们需要在 models.py 文件中添加生成图像和文本嵌入的方法。



1.  # /services/models.py
2.  # convert images to vector using CLIP
3.  async def clip_image_embeddings(image: Image.Image):
4.      try:
5.          inputs = clip_processor(images=image, return_tensors="pt", padding=True)
6.          outputs = clip_model.get_image_features(**inputs)
7.          return outputs.detach().cpu().numpy().flatten().tolist()
8.      except Exception as e:
9.          print(f"Error generating embeddings: {e}")
10.          return None
11.  # convert text to to vector
12.  async def clip_text_embeddings(description: str):
13.      try:
14.          inputs = clip_processor([description], padding=True, return_tensors="pt")
15.          outputs = clip_model.get_text_features(**inputs)
16.          return outputs.detach().cpu().numpy().flatten().tolist()
17.      except Exception as e:
18.          print(f"Error generating embeddings: {e}")
19.          return None


对于所有模型,你需要声明类似的方法:一个用于从图像生成嵌入(clip_image_embeddings),另一个用于从文本生成嵌入(clip_text_embeddings)。

outputs.detach().cpu().numpy().flatten().tolist() 链是一种将 pytorch tensors 转换为更可用格式的常见操作:

  • .detach():从计算图中删除张量,因为我们不再需要计算梯度
  • .cpu():将 tensors 从 GPU 移动到 CPU,因为 numpy 仅支持 CPU
  • .numpy():将 tensors 转换为 numPy 数组。
  • .flatten():转换为 1D 数组。
  • .toList():转换为 Python 列表。

此操作将多维 tensor 转换为可用于嵌入操作的纯数字列表。

现在让我们看一些 CLIP 替代方案。

竞争对手 1:JinaCLIP

JinaCLIP 是 Jina AI 开发的 CLIP 变体,专门用于改进多模态应用中的图像和文本搜索。它通过增加图像和文本表示的灵活性来优化 CLIP 性能。

与原始 OpenAI CLIP 模型相比,JinaCLIP 在文本转文本、文本转图像、图像转文本和图像转图像任务中表现更好,如下图所示:

ModelText-TextText-to-ImageImage-to-TextImage-Image
jina-clip-v10.4290.8990.8030.916
openai-clip-vit-b160.1620.8810.7560.816
%increase vs OpenAI CLIP165%2%6%12%

它能够提高不同类型查询的精度,因此非常适合需要更精确、更详细分析的任务。

你可以在此处阅读有关 JinaCLIP 的更多信息。

要在我们的应用中使用 JinaCLIP 并生成嵌入,我们需要声明以下方法:



1.  [ ]
2.  # /services/models.py
3.  # convert images to vector using JinaClip model
4.  async def jina_image_embeddings(image: Image.Image):
5.      try:
6.          image_embeddings = jina_model.encode_image([image])
7.          return image_embeddings[0].tolist()
8.      except Exception as e:
9.          print(f"Error generating embeddings: {e}")
10.          return None
11.  # convert text to vector
12.  async def jina_text_embeddings(description: str):
13.      try:
14.          text_embeddings = jina_model.encode_text(description)
15.          return text_embeddings.tolist()
16.      except Exception as e:
17.          print(f"Error generating embeddings: {e}")
18.          return None


竞争对手 2:Cohere Image Embeddings V3

Cohere 开发了一种名为 Embed-3 的图像嵌入模型,它是 CLIP 的直接竞争对手。主要区别在于 Cohere 专注于企业数据(如图表、产品图像和设计文件)的表示。Embed-3 使用一种先进的架构,可以降低对文本数据的偏见风险,这目前是 CLIP 等其他多模态模型的劣势,因此它可以在文本和图像之间提供更精确的结果。

你可以在下方看到 Cohere 的图表,该图表显示了在这种数据中使用 Embed 3 与 CLIP 相比的改进结果:

有关更多信息,请访问 Embed3

就像我们对之前的模型所做的那样,让我们​​声明使用 Embed 3 的方法:



1.  [ ]
2.  # /services/models.py
3.  # convert images to vector using Cohere Embed model
4.  async def embed_image_embeddings(image: Image.Image):
5.      try:
6.          img_byte_arr = io.BytesIO()
7.          image.save(img_byte_arr, format="JPEG")
8.          img_byte_arr = img_byte_arr.getvalue()
9.          stringified_buffer = base64.b64encode(img_byte_arr).decode("utf-8")
10.          content_type = "image/jpeg"
11.          image_base64 = f"data:{content_type};base64,{stringified_buffer}"
12.          response = co.embed(
13.              model="embed-english-v3.0",
14.              input_type="image",
15.              embedding_types=["float"],
16.              images=[image_base64],
17.          )
18.       return response.embeddings.float_[0]
19.      except Exception as e:
20.          print(f"Error generating embeddings: {e}")
21.          return None
22.  # convert text to vector
23.  async def embed_text_embeddings(description: str):
24.      try:
25.          response = co.embed(
26.              texts=[description],
27.              model="embed-english-v3.0",
28.              input_type="classification",
29.              embedding_types=["float"],
30.          )
31.       return response.embeddings.float_[0]
32.      except Exception as e:
33.          print(f"Error generating embeddings: {e}")
34.          return None


准备好函数后,让我们通过在文件 index_data.py 中添加以下代码来在 Elasticsearch 中索引数据集:



1.  [ ]
2.  # dependencies
3.  import asyncio
4.  import base64
5.  import os
6.  from PIL import Image
7.  from services.elasticsearch import index_images
8.  from services.models import (
9.      clip_image_embeddings,
10.      embed_image_embeddings,
11.      jina_image_embeddings,
12.  )
13.  # function to encode images
14.  def encode_image_to_base64(image_path):
15.      with open(image_path, "rb") as img_file:
16.          return base64.b64encode(img_file.read()).decode("utf-8")
17.  async def main():
18.      # folder with images
19.      folder_path = "./data"
20.      jina_obj_arr = []
21.      embed_obj_arr = []
22.      clip_obj_arr = []
23.      for filename in os.listdir(folder_path):
24.          img_path = os.path.join(folder_path, filename)
25.          print(f"Processing {filename}...")
26.          try:
27.              image_data = Image.open(img_path)
28.              # generating images embeddings
29.              clip_result, embed_result, jina_result = await asyncio.gather(
30.                  clip_image_embeddings(image_data),
31.                  embed_image_embeddings(image_data),
32.                  jina_image_embeddings(image_data),
33.              )
34.              image_base64 = encode_image_to_base64(img_path)
35.              # building documents
36.              jina_obj_arr.append(
37.                  {
38.                      "image_name": filename,
39.                      "image_embedding": jina_result,
40.                      "image_data": image_base64,
41.                  }
42.              )
43.              embed_obj_arr.append(
44.                  {
45.                      "image_name": filename,
46.                      "image_embedding": embed_result,
47.                      "image_data": image_base64,
48.                  }
49.              )
50.              clip_obj_arr.append(
51.                  {
52.                      "image_name": filename,
53.                      "image_embedding": clip_result,
54.                      "image_data": image_base64,
55.                  }
56.              )
57.          except Exception as e:
58.              print(f"Error with {filename}: {e}")
59.      print("Indexing images in Elasticsearch...")
60.      # indexing images
61.      jina_count, _ = index_images(jina_index, jina_obj_arr)
62.      cohere_count, _ = index_images(embed_index, cohere_obj_arr)
63.      openai_count, _ = index_images(clip_index, openai_obj_arr)
64.      print("Cohere count: ", cohere_count)
65.      print("Jina count: ", jina_count)
66.      print("OpenAI count: ", openai_count)
67.  if __name__ == "__main__":
68.      asyncio.run(main())


使用以下命令对文档进行索引:

python index_data.py

一旦数据集被索引,我们就可以创建 UI。

测试 UI

创建 UI

我们将使用 Streamlit 构建 UI 并并排比较这三种替代方案。

要构建 UI,我们首先将导入和依赖项添加到文件 app.py:



1.  [ ]
2.  # app.py
3.  import asyncio
4.  import base64
5.  from io import BytesIO
6.  import streamlit as st
7.  from PIL import Image
8.  from services.elasticsearch import get_all_query, knn_search
9.  # declared functions imports
10.  from services.models import (
11.      clip_image_embeddings,
12.      clip_text_embeddings,
13.      embed_image_embeddings,
14.      embed_text_embeddings,
15.      jina_image_embeddings,
16.      jina_text_embeddings,
17.  )


对于此示例,我们将使用两个视图;一个用于图像搜索,另一个用于查看图像数据集:



1.  [ ]
2.  # app.py
3.  if "selected_view" not in st.session_state:
4.      st.session_state.selected_view = "Index"
5.  def change_view(view):
6.      st.session_state.selected_view = view
7.  st.sidebar.title("Menu")
8.  if st.sidebar.button("Search image"):
9.      change_view("Index")
10.  if st.sidebar.button("All images"):
11.      change_view("Images")


让我们添加搜索图像的视图代码:



1.  [ ]
2.  if st.session_state.selected_view == "Index":
3.      # Index page
4.      st.title("Image Search")
5.      col1, col_or, col2 = st.columns([2, 1, 2])
6.      uploaded_image = None
7.      with col1:
8.          uploaded_image = st.file_uploader("Upload image", type=["jpg", "jpeg", "png"])
9.      with col_or:
10.          st.markdown(
11.              "<h3 style='text-align: center; margin-top: 50%;'>OR</h3>",
12.              unsafe_allow_html=True,
13.          )
14.      input_text = None
15.      with col2:
16.          st.markdown(
17.              "<div style='display: flex; margin-top: 3rem;  align-items: center; height: 100%; justify-content: center;'>",
18.              unsafe_allow_html=True,
19.          )
20.          input_text = st.text_input("Type text")
21.          st.markdown("</div>", unsafe_allow_html=True)
22.      st.write("")
23.      st.write("")
24.      search_button = st.markdown(
25.          """
26.          <style>
27.              .stButton>button {
28.                  width: 50%;
29.                  height: 50px;
30.                  font-size: 20px;
31.                  margin: 0 auto;
32.                  display: block;
33.              }
34.          </style>
35.          """,
36.          unsafe_allow_html=True,
37.      )
38.      submit_button = st.button("Search")
39.      if uploaded_image:
40.          st.image(uploaded_image, caption="Uploaded Image", use_container_width=True)
41.      if submit_button:
42.          if uploaded_image or input_text:
43.              async def fetch_embeddings():
44.                  data = None
45.                  if uploaded_image:
46.                      image = Image.open(uploaded_image)
47.                      data = image
48.                  elif input_text:
49.                      data = input_text
50.                  # Getting image or text embeddings
51.                  if uploaded_image:
52.                      openai_result, cohere_result, jina_result = await asyncio.gather(
53.                          clip_image_embeddings(data),
54.                          embed_image_embeddings(data),
55.                          jina_image_embeddings(data),
56.                      )
57.                  elif input_text:
58.                      openai_result, cohere_result, jina_result = await asyncio.gather(
59.                          clip_text_embeddings(data),
60.                          embed_text_embeddings(data),
61.                          jina_text_embeddings(data),
62.                      )
63.                  return openai_result, cohere_result, jina_result
64.              results = asyncio.run(fetch_embeddings())
65.              openai_result, cohere_result, jina_result = results
66.              if openai_result and cohere_result and jina_result:
67.                  # calling knn query
68.                  clip_search_results = knn_search("clip-images", openai_result, 5)
69.                  jina_search_results = knn_search("jina-images", jina_result, 5)
70.                  embed_search_results = knn_search("embed-images", cohere_result, 5)
71.                  clip_search_results = clip_search_results["hits"]["hits"]
72.                  jina_search_results = jina_search_results["hits"]["hits"]
73.                  embed_search_results = embed_search_results["hits"]["hits"]
74.                  st.subheader("Search Results")
75.                  col1, spacer1, col2, spacer2, col3 = st.columns([3, 0.2, 3, 0.2, 3])
76.                  def print_results(results):
77.                      for hit in results:
78.                          image_data = base64.b64decode(hit["_source"]["image_data"])
79.                          image = Image.open(BytesIO(image_data))
80.                          st.image(image, use_container_width=True)
81.                          st.write("score: ", hit["_score"])
82.                  # printing results
83.                  with col1:
84.                      st.write("CLIP")
85.                      print_results(clip_search_results)
86.                  with col2:
87.                      st.write("JinaCLIP")
88.                      print_results(jina_search_results)
89.                  with col3:
90.                      st.write("Cohere")
91.                      print_results(embed_search_results)
92.          else:
93.              st.warning("Please upload an image or type text to search.")


现在,图像视图的代码:



1.  [ ]
2.  elif st.session_state.selected_view == "Images":
3.      # images page
4.      st.header("All images")
5.      # getting all images
6.      images = get_all_query("jina-images")
7.      hits = images["hits"]["hits"]
8.      columns = st.columns(5)
9.      for idx, hit in enumerate(hits):
10.          image_data = base64.b64decode(hit["_source"]["image_data"])
11.          image = Image.open(BytesIO(image_data))
12.          with columns[idx % 5]:
13.              st.image(image, use_container_width=True)


我们将使用以下命令运行该应用程序:

streamlit run app.py

使用 Elasticsearch 来进行图形搜索 - CLIP 替代品

由于多模态性,我们可以在图像数据库中根据文本(文本到图像的相似性)或图像(图像到图像的相似性)运行搜索。

使用 UI 搜索

为了比较这三种模型,我们将使用一个场景,即房地产网页希望通过允许用户使用图像或文本进行搜索来改善其搜索体验。我们将讨论每种模型提供的结果。

我们将上传 “rustic home” 的图片:

以下是搜索结果。如你所见,根据我们上传的图像,每个模型都生成了不同的结果:

此外,你还可以看到根据文本查找房屋特征的结果:

如果搜索 “modern”,这三个模型都会显示良好的结果。但是,JinaCLIP 和 Cohere 会在第一个位置显示相同的房屋。

功能比较

下面是本文中介绍的三个选项的主要功能和价格的摘要:

ModelCreated byEstimated PriceFeatures
CLIPOpenAI每次重复运行 0.00058 美元 (https://replicate.com/krthr/clip-embeddings)针对文本和图像的通用多模态模型;适用于无需特定训练的各种应用。
JinaCLIPJina AI每 100 万枚 Jina tokens 需 0.018 美元 (https://jina.ai/embeddings/)针对多模式应用优化的 CLIP 变体。提高了检索文本和图像的精度。
Embed-3CohereCohere 上每 100 万个 tokens 收费 0.10 美元,每份数据和图像收费 0.0001 美元(https://cohere.com/pricing)专注于企业数据。改进了图形和图表等复杂视觉数据的检索。

如果你要搜索长图像描述,或者想要进行文本转文本和图像转文本,则应放弃 CLIP,因为 JinaCLIP 和 Embed-3 都针对此用例进行了优化。

JinaCLIP 是一种通用模型,而 Cohere 的模型更侧重于企业数据,如产品或图表。

在数据上测试模型时,请确保涵盖:

  • 你感兴趣的所有模式:文本转图像、图像转文本、文本转文本
  • 长图像描述和短图像描述
  • 相似概念匹配(同一类型对象的不同图像)
  • 负面
    • 硬负面:与预期输出相似但仍然错误
    • 简单负面:与预期输出不相似且错误
  • 具有挑战性的场景
    • 不同的角度/视角
    • 各种照明条件
    • 抽象概念(“modern”、“cozy”、“luxurious”)
  • 特定领域案例
    • 技术图表或图表(尤其是 Embed-3)
    • 产品变化(颜色、尺寸、样式)

结论

虽然 CLIP 是进行图像相似性搜索时的首选模型,但在某些情况下,商业和非商业替代方案都可以表现得更好。

JinaCLIP 是一款强大的一体化工具,据称在文本到文本嵌入方面比 CLIP 更精确。

Embed-3 遵循 Cohere 的路线,通过使用典型的业务文档使用真实数据训练模型来满足业务客户的需求。

在我们的小实验中,我们可以看到 JinaClip 和 Cohere 都显示了有趣的图像到图像和文本到图像结果,并且在这些类型的任务中表现与 CLIP 非常相似。

Elasticsearch 允许你搜索嵌入,将向量搜索与全文搜索相结合,使你能够搜索图像和其中的文本。

想要获得 Elastic 认证?了解下一次 Elasticsearch 工程师培训的时间!

Elasticsearch 包含新功能,可帮助你为你的用例构建最佳搜索解决方案。深入了解我们的示例笔记本以了解更多信息,开始免费云试用,或立即在本地机器上试用 Elastic。

原文:Exploring CLIP alternatives - Elasticsearch Labs