GLM-4v-9b多模态教程:图文联合embedding生成、跨模态相似度计算与检索应用

1. 引言:为什么你需要关注图文联合embedding?

想象一下这个场景:你是一家电商公司的运营,每天要处理成千上万的商品图片和描述文案。你想快速找到所有“红色连衣裙”的图片,但系统只能通过文字标签搜索,很多图片没有打上“红色”或“连衣裙”的标签,结果漏掉了一大半。

或者,你是一个内容创作者,手头有一堆素材图片,想找一张“表达孤独感的城市夜景”配图,但光靠关键词搜索,出来的都是“夜景”、“城市”,完全不对味。

这就是传统搜索的痛点——文字和图片是割裂的。而GLM-4v-9b带来的图文联合embedding技术,正是为了解决这个问题。它能让机器像人一样,同时理解图片的内容和文字的含义,并把它们映射到同一个“语义空间”里进行比较。

简单来说,有了它,你可以:

  • 用一段文字描述,直接搜到最匹配的图片
  • 用一张图片,反向搜索到描述它的文字
  • 实现真正的“以图搜图”、“以文搜图”,甚至“以图搜文”

本教程将手把手带你玩转GLM-4v-9b的这项核心能力。即使你之前没接触过多模态模型,跟着步骤走,也能在半小时内搭建起自己的跨模态检索系统。

2. 环境准备:一条命令搞定部署

开始之前,我们先确保环境就绪。GLM-4v-9b的部署已经非常简化,特别是通过CSDN星图这样的平台。

2.1 基础环境要求

你的机器需要满足以下条件:

  • 操作系统:Linux (Ubuntu 20.04+ 推荐) 或 Windows (WSL2)
  • 显卡:至少24GB显存 (如RTX 4090) 用于FP16精度运行;如果使用INT4量化,9GB显存 (如RTX 4060 Ti 16GB) 即可
  • 内存:32GB RAM 或以上
  • 磁盘空间:50GB 可用空间 (用于存放模型权重和依赖)

2.2 快速部署方案

对于大多数开发者,我推荐直接使用预置的Docker镜像或云平台服务,避免环境配置的麻烦。

方案一:使用CSDN星图镜像(最快) 如果你在CSDN星图平台,可以直接搜索“GLM-4v-9b”镜像,一键部署。平台已经预配置好了所有环境,包括transformers库、vLLM加速等。

方案二:本地Docker部署 如果你习惯本地开发,可以使用官方提供的Docker镜像:

# 拉取镜像
docker pull registry.cn-beijing.aliyuncs.com/glm/glm-4v-9b:latest

# 运行容器
docker run -it --gpus all \
  -p 7860:7860 \
  -v /path/to/your/data:/data \
  registry.cn-beijing.aliyuncs.com/glm/glm-4v-9b:latest

方案三:手动安装(适合定制化需求) 如果你需要更灵活的控制,可以手动安装:

# 1. 创建虚拟环境
conda create -n glm4v python=3.10
conda activate glm4v

# 2. 安装PyTorch (根据你的CUDA版本选择)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# 3. 安装transformers和视觉相关库
pip install transformers accelerate pillow
pip install git+https://github.com/huggingface/transformers

# 4. 安装vLLM用于加速推理(可选但推荐)
pip install vllm

2.3 验证安装

安装完成后,用一段简单的代码测试环境是否正常:

import torch
from transformers import AutoModel, AutoTokenizer

# 检查CUDA是否可用
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"GPU count: {torch.cuda.device_count()}")
if torch.cuda.is_available():
    print(f"Current GPU: {torch.cuda.get_device_name(0)}")
    print(f"GPU memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB")

如果一切正常,你会看到类似这样的输出:

CUDA available: True
GPU count: 1
Current GPU: NVIDIA GeForce RTX 4090
GPU memory: 24.00 GB

3. 核心概念:图文embedding到底是什么?

在深入代码之前,我们先花几分钟搞清楚几个关键概念。别担心,我用最直白的方式解释。

3.1 什么是embedding?

你可以把embedding理解成一种“数学指纹”。就像每个人有独特的指纹一样,每段文字、每张图片经过模型处理,都会生成一串数字(通常是512或1024维的向量),这串数字就是它的embedding。

关键点在于:语义相似的内容,它们的embedding在数学空间里的距离也很近

举个例子:

  • “猫”的embedding和“猫咪”的embedding会很接近
  • “狗”的embedding和“宠物”的embedding也比较接近
  • 一张猫的图片和“猫”这个文字的embedding也会很接近

这就是跨模态检索的基础——不同模态(图、文)但语义相同的内容,被映射到了相近的位置。

3.2 GLM-4v-9b如何生成联合embedding?

GLM-4v-9b的巧妙之处在于,它用一个模型同时处理图片和文字:

  1. 图片处理:通过视觉编码器(ViT)把图片转换成视觉特征
  2. 文字处理:通过语言模型把文字转换成文本特征
  3. 特征融合:通过交叉注意力机制,让图片特征和文字特征相互“交流”
  4. 统一输出:最终输出一个融合了图文信息的联合embedding

这个过程是端到端训练的,意味着模型在学习过程中就学会了如何让图片和文字的embedding在同一个空间里对齐。

3.3 为什么1120×1120分辨率很重要?

你可能会注意到GLM-4v-9b特别强调了1120×1120的高分辨率支持。这在实际应用中非常实用:

  • 小字识别:图表中的坐标轴标签、表格里的数字都能看清
  • 细节保留:图片中的纹理、物体的细微特征不会丢失
  • 文档处理:扫描的PDF、截图中的文字都能准确识别

传统模型往往把图片缩放到224×224或384×384,很多细节就糊掉了。GLM-4v-9b保持高分辨率输入,让它在处理复杂图片时优势明显。

4. 实战开始:生成你的第一个图文embedding

理论讲完了,现在我们来动手实践。我会带你一步步生成图片和文字的embedding。

4.1 加载模型和处理器

首先,我们需要加载GLM-4v-9b模型和对应的处理器:

import torch
from PIL import Image
from transformers import AutoModel, AutoTokenizer, AutoProcessor

# 设置设备
device = "cuda" if torch.cuda.is_available() else "cpu"

# 加载模型和处理器
model_name = "THUDM/glm-4v-9b"

print("正在加载tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

print("正在加载processor...")
processor = AutoProcessor.from_pretrained(model_name, trust_remote_code=True)

print("正在加载模型...")
model = AutoModel.from_pretrained(
    model_name,
    torch_dtype=torch.float16,  # 使用半精度减少显存占用
    device_map="auto",  # 自动分配到可用GPU
    trust_remote_code=True
)
model.eval()  # 设置为评估模式
print("模型加载完成!")

重要提示:第一次运行时会下载模型权重(约18GB),请确保网络通畅和磁盘空间充足。如果下载慢,可以考虑先下载到本地再加载。

4.2 准备测试数据

我们来准备一些测试用的图片和文字。你可以用自己的图片,或者用我提供的示例:

import requests
from io import BytesIO
import matplotlib.pyplot as plt

# 示例1:一张猫的图片
cat_image_url = "https://images.unsplash.com/photo-1514888286974-6d03bde4ba4f"
cat_text = "一只橘猫坐在窗台上晒太阳"

# 示例2:一张风景图片
landscape_image_url = "https://images.unsplash.com/photo-1501854140801-50d01698950b"
landscape_text = "山间湖泊,远处有雪山,水面平静如镜"

# 示例3:一张食物图片
food_image_url = "https://images.unsplash.com/photo-1565958011703-44f9829ba187"
food_text = "一盘新鲜的沙拉,有西红柿、黄瓜和生菜"

def download_image(url):
    """下载图片并转换为PIL Image"""
    response = requests.get(url)
    img = Image.open(BytesIO(response.content))
    return img

# 下载图片
print("下载测试图片...")
cat_img = download_image(cat_image_url)
landscape_img = download_image(landscape_image_url)
food_img = download_image(food_image_url)

# 显示图片(可选)
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(cat_img)
axes[0].set_title("猫")
axes[0].axis('off')

axes[1].imshow(landscape_img)
axes[1].set_title("风景")
axes[1].axis('off')

axes[2].imshow(food_img)
axes[2].set_title("食物")
axes[2].axis('off')

plt.tight_layout()
plt.show()

4.3 生成图片embedding

现在我们来生成图片的embedding。GLM-4v-9b提供了专门的方法来提取视觉特征:

def get_image_embedding(image, model, processor):
    """
    获取图片的embedding
    
    参数:
        image: PIL Image对象
        model: 加载好的GLM-4v-9b模型
        processor: 对应的处理器
    
    返回:
        embedding: 图片的embedding向量
    """
    # 使用处理器准备输入
    inputs = processor(images=image, return_tensors="pt")
    
    # 将输入移动到GPU
    inputs = {k: v.to(device) for k, v in inputs.items()}
    
    # 生成embedding
    with torch.no_grad():  # 不计算梯度,加快推理速度
        outputs = model.get_image_features(**inputs)
    
    # 提取embedding并归一化(重要!)
    embedding = outputs.last_hidden_state.mean(dim=1)  # 取平均池化
    embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)  # L2归一化
    
    return embedding.cpu().numpy()  # 转回numpy方便后续处理

# 为三张图片生成embedding
print("生成图片embedding...")
cat_img_emb = get_image_embedding(cat_img, model, processor)
landscape_img_emb = get_image_embedding(landscape_img, model, processor)
food_img_emb = get_image_embedding(food_img, model, processor)

print(f"猫图片embedding形状: {cat_img_emb.shape}")
print(f"风景图片embedding形状: {landscape_img_emb.shape}")
print(f"食物图片embedding形状: {food_img_emb.shape}")

你会看到输出类似:

猫图片embedding形状: (1, 2048)
风景图片embedding形状: (1, 2048)
食物图片embedding形状: (1, 2048)

这里的2048就是embedding的维度。每个图片都被转换成了一个2048维的向量。

4.4 生成文字embedding

同样地,我们也可以为文字生成embedding:

def get_text_embedding(text, model, tokenizer):
    """
    获取文字的embedding
    
    参数:
        text: 文本字符串
        model: 加载好的GLM-4v-9b模型
        tokenizer: 对应的tokenizer
    
    返回:
        embedding: 文本的embedding向量
    """
    # 使用tokenizer编码文本
    inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
    
    # 将输入移动到GPU
    inputs = {k: v.to(device) for k, v in inputs.items()}
    
    # 生成embedding
    with torch.no_grad():
        outputs = model.get_text_features(**inputs)
    
    # 提取embedding并归一化
    embedding = outputs.last_hidden_state.mean(dim=1)  # 取平均池化
    embedding = torch.nn.functional.normalize(embedding, p=2, dim=1)  # L2归一化
    
    return embedding.cpu().numpy()

# 为三段文字生成embedding
print("生成文字embedding...")
cat_text_emb = get_text_embedding(cat_text, model, tokenizer)
landscape_text_emb = get_text_embedding(landscape_text, model, tokenizer)
food_text_emb = get_text_embedding(food_text, model, tokenizer)

print(f"猫文字embedding形状: {cat_text_emb.shape}")
print(f"风景文字embedding形状: {landscape_text_emb.shape}")
print(f"食物文字embedding形状: {food_text_emb.shape}")

注意:文字embedding的维度也是2048,和图片embedding的维度一致。这是实现跨模态检索的关键——图片和文字在同一个向量空间里。

5. 计算相似度:让图片和文字“对话”

有了embedding,我们就可以计算相似度了。相似度越高,说明内容越相关。

5.1 理解相似度计算

在embedding空间里,我们通常用余弦相似度来衡量两个向量的相似程度。它的取值范围是[-1, 1]:

  • 1:完全相似(方向相同)
  • 0:不相关(垂直)
  • -1:完全相反(方向相反)

计算公式很简单:两个向量的点积除以它们模长的乘积。

5.2 实现相似度计算

我们来写一个计算余弦相似度的函数:

import numpy as np

def cosine_similarity(vec1, vec2):
    """
    计算两个向量的余弦相似度
    
    参数:
        vec1, vec2: 两个向量,形状为(1, n)或(n,)
    
    返回:
        similarity: 余弦相似度,范围[-1, 1]
    """
    # 确保是1维向量
    if len(vec1.shape) > 1:
        vec1 = vec1.flatten()
    if len(vec2.shape) > 1:
        vec2 = vec2.flatten()
    
    # 计算点积
    dot_product = np.dot(vec1, vec2)
    
    # 计算模长
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    
    # 避免除零
    if norm1 == 0 or norm2 == 0:
        return 0
    
    # 计算余弦相似度
    similarity = dot_product / (norm1 * norm2)
    
    return similarity

# 测试:计算猫图片和猫文字的相似度
cat_similarity = cosine_similarity(cat_img_emb, cat_text_emb)
print(f"猫图片 vs 猫文字描述 相似度: {cat_similarity:.4f}")

# 计算猫图片和风景文字的相似度(应该较低)
cat_landscape_similarity = cosine_similarity(cat_img_emb, landscape_text_emb)
print(f"猫图片 vs 风景文字描述 相似度: {cat_landscape_similarity:.4f}")

正常情况下的输出应该是:

猫图片 vs 猫文字描述 相似度: 0.85xx (较高,接近1)
猫图片 vs 风景文字描述 相似度: 0.2xxx (较低,接近0)

5.3 批量计算相似度矩阵

在实际应用中,我们经常需要计算多个图片和多个文字之间的相似度。我们来写一个更实用的函数:

def compute_similarity_matrix(image_embeddings, text_embeddings):
    """
    计算图片embedding和文字embedding之间的相似度矩阵
    
    参数:
        image_embeddings: 列表,包含多个图片的embedding
        text_embeddings: 列表,包含多个文字的embedding
    
    返回:
        similarity_matrix: 相似度矩阵,shape=(n_images, n_texts)
    """
    n_images = len(image_embeddings)
    n_texts = len(text_embeddings)
    
    # 初始化相似度矩阵
    similarity_matrix = np.zeros((n_images, n_texts))
    
    # 计算每对图片和文字的相似度
    for i in range(n_images):
        for j in range(n_texts):
            similarity_matrix[i, j] = cosine_similarity(
                image_embeddings[i], text_embeddings[j]
            )
    
    return similarity_matrix

# 准备数据
image_embeddings = [cat_img_emb, landscape_img_emb, food_img_emb]
text_embeddings = [cat_text_emb, landscape_text_emb, food_text_emb]

image_names = ["猫图片", "风景图片", "食物图片"]
text_descriptions = ["猫文字", "风景文字", "食物文字"]

# 计算相似度矩阵
similarity_matrix = compute_similarity_matrix(image_embeddings, text_embeddings)

print("相似度矩阵:")
print("行: 图片, 列: 文字")
print("=" * 50)
print(f"{'':15}", end="")
for text in text_descriptions:
    print(f"{text:15}", end="")
print()

for i, img_name in enumerate(image_names):
    print(f"{img_name:15}", end="")
    for j in range(len(text_descriptions)):
        print(f"{similarity_matrix[i, j]:.4f}        ", end="")
    print()

你会看到一个3x3的矩阵,对角线上的值应该最高(图片和对应的文字描述),非对角线上的值应该较低。

6. 构建跨模态检索系统

现在到了最实用的部分——用我们学到的知识构建一个真正的跨模态检索系统。

6.1 系统设计思路

一个完整的跨模态检索系统通常包括:

  1. 建库阶段:处理所有图片,生成embedding并存储
  2. 检索阶段:接收查询(文字或图片),计算相似度,返回最匹配的结果
  3. 排序阶段:按相似度排序,返回Top-K结果

6.2 实现图片库管理

我们先实现一个简单的图片库管理类:

import os
import pickle
from pathlib import Path
from typing import List, Dict, Tuple
import numpy as np

class ImageRetrievalSystem:
    """跨模态图片检索系统"""
    
    def __init__(self, model, processor, tokenizer):
        """
        初始化检索系统
        
        参数:
            model: GLM-4v-9b模型
            processor: 图片处理器
            tokenizer: 文本tokenizer
        """
        self.model = model
        self.processor = processor
        self.tokenizer = tokenizer
        self.image_paths = []  # 图片路径列表
        self.image_embeddings = []  # 图片embedding列表
        self.image_metadata = []  # 图片元数据(可选)
        
    def add_image(self, image_path: str, image: Image.Image = None, metadata: Dict = None):
        """
        添加图片到库中
        
        参数:
            image_path: 图片路径
            image: PIL Image对象(如果提供则直接使用)
            metadata: 图片元数据
        """
        # 加载图片(如果未提供)
        if image is None:
            try:
                image = Image.open(image_path).convert("RGB")
            except Exception as e:
                print(f"无法加载图片 {image_path}: {e}")
                return False
        
        # 生成embedding
        try:
            embedding = get_image_embedding(image, self.model, self.processor)
        except Exception as e:
            print(f"生成embedding失败 {image_path}: {e}")
            return False
        
        # 保存到库中
        self.image_paths.append(image_path)
        self.image_embeddings.append(embedding)
        self.image_metadata.append(metadata or {})
        
        print(f"已添加图片: {image_path} (embedding形状: {embedding.shape})")
        return True
    
    def add_image_directory(self, directory_path: str, extensions: List[str] = None):
        """
        批量添加目录中的所有图片
        
        参数:
            directory_path: 目录路径
            extensions: 支持的图片扩展名,默认为['.jpg', '.jpeg', '.png', '.bmp']
        """
        if extensions is None:
            extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.webp']
        
        directory = Path(directory_path)
        if not directory.exists():
            print(f"目录不存在: {directory_path}")
            return
        
        image_files = []
        for ext in extensions:
            image_files.extend(directory.glob(f"*{ext}"))
            image_files.extend(directory.glob(f"*{ext.upper()}"))
        
        print(f"找到 {len(image_files)} 张图片")
        
        added_count = 0
        for img_path in image_files:
            if self.add_image(str(img_path)):
                added_count += 1
        
        print(f"成功添加 {added_count} 张图片到库中")
    
    def search_by_text(self, query_text: str, top_k: int = 5):
        """
        用文字搜索图片
        
        参数:
            query_text: 查询文本
            top_k: 返回最相似的K个结果
        
        返回:
            results: 列表,每个元素为(图片路径, 相似度, 元数据)
        """
        # 生成查询文本的embedding
        query_embedding = get_text_embedding(query_text, self.model, self.tokenizer)
        
        # 计算相似度
        similarities = []
        for img_emb in self.image_embeddings:
            sim = cosine_similarity(query_embedding, img_emb)
            similarities.append(sim)
        
        # 获取Top-K结果
        similarities = np.array(similarities).flatten()
        top_indices = np.argsort(similarities)[-top_k:][::-1]  # 从高到低排序
        
        # 构建结果
        results = []
        for idx in top_indices:
            results.append((
                self.image_paths[idx],
                similarities[idx],
                self.image_metadata[idx]
            ))
        
        return results
    
    def search_by_image(self, query_image: Image.Image, top_k: int = 5):
        """
        用图片搜索图片(以图搜图)
        
        参数:
            query_image: 查询图片
            top_k: 返回最相似的K个结果
        
        返回:
            results: 列表,每个元素为(图片路径, 相似度, 元数据)
        """
        # 生成查询图片的embedding
        query_embedding = get_image_embedding(query_image, self.model, self.processor)
        
        # 计算相似度
        similarities = []
        for img_emb in self.image_embeddings:
            sim = cosine_similarity(query_embedding, img_emb)
            similarities.append(sim)
        
        # 获取Top-K结果
        similarities = np.array(similarities).flatten()
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        
        # 构建结果
        results = []
        for idx in top_indices:
            results.append((
                self.image_paths[idx],
                similarities[idx],
                self.image_metadata[idx]
            ))
        
        return results
    
    def save_index(self, filepath: str):
        """
        保存索引到文件
        
        参数:
            filepath: 保存路径
        """
        data = {
            'image_paths': self.image_paths,
            'image_embeddings': self.image_embeddings,
            'image_metadata': self.image_metadata
        }
        
        with open(filepath, 'wb') as f:
            pickle.dump(data, f)
        
        print(f"索引已保存到: {filepath}")
    
    def load_index(self, filepath: str):
        """
        从文件加载索引
        
        参数:
            filepath: 索引文件路径
        """
        with open(filepath, 'rb') as f:
            data = pickle.load(f)
        
        self.image_paths = data['image_paths']
        self.image_embeddings = data['image_embeddings']
        self.image_metadata = data['image_metadata']
        
        print(f"已加载索引,包含 {len(self.image_paths)} 张图片")

6.3 使用检索系统

现在我们来实际使用这个检索系统:

# 初始化检索系统
retrieval_system = ImageRetrievalSystem(model, processor, tokenizer)

# 添加一些测试图片(这里用之前下载的图片)
# 在实际应用中,你可以添加自己的图片目录
retrieval_system.add_image("cat_image.jpg", cat_img, {"category": "animal"})
retrieval_system.add_image("landscape_image.jpg", landscape_img, {"category": "scenery"})
retrieval_system.add_image("food_image.jpg", food_img, {"category": "food"})

# 用文字搜索图片
print("\n=== 文字搜索测试 ===")
query_text = "一只可爱的宠物"
results = retrieval_system.search_by_text(query_text, top_k=2)

print(f"查询: '{query_text}'")
print("搜索结果:")
for i, (path, score, metadata) in enumerate(results):
    print(f"  {i+1}. {Path(path).name} (相似度: {score:.4f}, 类别: {metadata.get('category', 'N/A')})")

# 用图片搜索图片(以图搜图)
print("\n=== 图片搜索测试 ===")
# 用猫图片搜索
results = retrieval_system.search_by_image(cat_img, top_k=2)

print("查询: 猫图片")
print("搜索结果:")
for i, (path, score, metadata) in enumerate(results):
    print(f"  {i+1}. {Path(path).name} (相似度: {score:.4f}, 类别: {metadata.get('category', 'N/A')})")

6.4 实际应用示例:电商商品搜索

让我们看一个更实际的例子——电商商品图片搜索:

# 模拟一个电商商品库
def setup_ecommerce_example():
    """设置电商商品搜索示例"""
    
    # 创建新的检索系统
    ecommerce_system = ImageRetrievalSystem(model, processor, tokenizer)
    
    # 模拟添加商品图片(在实际应用中,这里应该是真实的商品图片)
    # 我们使用一些示例图片URL来模拟
    product_images = [
        {
            "path": "red_dress_1.jpg",
            "url": "https://images.unsplash.com/photo-1595777457583-95e059d581b8",
            "metadata": {"category": "clothing", "color": "red", "type": "dress", "price": 299}
        },
        {
            "path": "blue_jeans_1.jpg", 
            "url": "https://images.unsplash.com/photo-1542272604-787c3835535d",
            "metadata": {"category": "clothing", "color": "blue", "type": "jeans", "price": 199}
        },
        {
            "path": "white_sneakers_1.jpg",
            "url": "https://images.unsplash.com/photo-1549298916-b41d501d3772",
            "metadata": {"category": "shoes", "color": "white", "type": "sneakers", "price": 599}
        },
        {
            "path": "black_backpack_1.jpg",
            "url": "https://images.unsplash.com/photo-1553062407-98eeb64c6a62",
            "metadata": {"category": "bags", "color": "black", "type": "backpack", "price": 399}
        },
        {
            "path": "red_dress_2.jpg",
            "url": "https://images.unsplash.com/photo-1591047139829-d91aecb6caea",
            "metadata": {"category": "clothing", "color": "red", "type": "dress", "price": 459}
        }
    ]
    
    print("正在构建电商商品库...")
    for product in product_images:
        try:
            # 下载图片
            img = download_image(product["url"])
            # 添加到系统
            ecommerce_system.add_image(
                product["path"], 
                img, 
                product["metadata"]
            )
        except Exception as e:
            print(f"添加商品失败 {product['path']}: {e}")
    
    return ecommerce_system

# 设置电商示例
ecommerce_system = setup_ecommerce_example()

# 测试搜索
print("\n=== 电商商品搜索测试 ===")

# 测试1:搜索红色连衣裙
print("\n1. 搜索'红色连衣裙':")
results = ecommerce_system.search_by_text("红色连衣裙", top_k=3)
for i, (path, score, metadata) in enumerate(results):
    print(f"   {i+1}. {metadata.get('type', '未知')} - {metadata.get('color', '未知颜色')}色 "
          f"(价格: ¥{metadata.get('price', 0)}, 相似度: {score:.4f})")

# 测试2:搜索休闲鞋
print("\n2. 搜索'休闲运动鞋':")
results = ecommerce_system.search_by_text("休闲运动鞋", top_k=3)
for i, (path, score, metadata) in enumerate(results):
    print(f"   {i+1}. {metadata.get('type', '未知')} - {metadata.get('color', '未知颜色')}色 "
          f"(价格: ¥{metadata.get('price', 0)}, 相似度: {score:.4f})")

# 测试3:以图搜图(用红色连衣裙图片搜索)
print("\n3. 以图搜图(红色连衣裙):")
# 使用第一件红色连衣裙的图片进行搜索
red_dress_img = download_image("https://images.unsplash.com/photo-1595777457583-95e059d581b8")
results = ecommerce_system.search_by_image(red_dress_img, top_k=3)
for i, (path, score, metadata) in enumerate(results):
    print(f"   {i+1}. {metadata.get('type', '未知')} - {metadata.get('color', '未知颜色')}色 "
          f"(价格: ¥{metadata.get('price', 0)}, 相似度: {score:.4f})")

这个示例展示了如何用GLM-4v-9b构建一个电商商品搜索系统。你可以看到,即使用户输入的是自然语言描述(如"红色连衣裙"),系统也能找到相关的商品图片。

7. 性能优化与实用技巧

在实际应用中,你可能会遇到性能问题。这里分享一些优化技巧:

7.1 批量处理加速

如果你需要处理大量图片,逐个处理会很慢。GLM-4v-9b支持批量处理:

def batch_get_image_embeddings(images, model, processor, batch_size=4):
    """
    批量获取图片embedding
    
    参数:
        images: PIL Image对象列表
        model: GLM-4v-9b模型
        processor: 图片处理器
        batch_size: 批处理大小
    
    返回:
        embeddings: embedding列表
    """
    embeddings = []
    
    # 分批处理
    for i in range(0, len(images), batch_size):
        batch = images[i:i+batch_size]
        
        # 准备批处理输入
        inputs = processor(images=batch, return_tensors="pt", padding=True)
        inputs = {k: v.to(device) for k, v in inputs.items()}
        
        # 批量生成embedding
        with torch.no_grad():
            outputs = model.get_image_features(**inputs)
        
        # 处理每个样本
        batch_embeddings = outputs.last_hidden_state
        for j in range(len(batch)):
            # 对每个样本取平均池化和归一化
            emb = batch_embeddings[j].mean(dim=0, keepdim=True)
            emb = torch.nn.functional.normalize(emb, p=2, dim=1)
            embeddings.append(emb.cpu().numpy())
        
        print(f"已处理 {min(i+batch_size, len(images))}/{len(images)} 张图片")
    
    return embeddings

7.2 使用向量数据库

当图片数量很大时(比如超过1万张),用Python列表存储和搜索会变得很慢。这时应该使用专门的向量数据库:

# 使用FAISS向量数据库的示例
import faiss

class FaissRetrievalSystem:
    """使用FAISS的检索系统"""
    
    def __init__(self, model, processor, tokenizer, embedding_dim=2048):
        self.model = model
        self.processor = processor
        self.tokenizer = tokenizer
        self.embedding_dim = embedding_dim
        
        # 创建FAISS索引
        self.index = faiss.IndexFlatIP(embedding_dim)  # 使用内积(余弦相似度)
        self.image_paths = []
        self.image_metadata = []
    
    def add_image(self, image_path, image=None, metadata=None):
        """添加图片到FAISS索引"""
        if image is None:
            image = Image.open(image_path).convert("RGB")
        
        # 生成embedding
        embedding = get_image_embedding(image, self.model, self.processor)
        
        # 添加到FAISS
        self.index.add(embedding.astype('float32'))
        self.image_paths.append(image_path)
        self.image_metadata.append(metadata or {})
        
        return True
    
    def search_by_text(self, query_text, top_k=5):
        """用文字搜索"""
        # 生成文字embedding
        query_embedding = get_text_embedding(query_text, self.model, self.tokenizer)
        
        # FAISS搜索
        distances, indices = self.index.search(query_embedding.astype('float32'), top_k)
        
        # 构建结果
        results = []
        for i, idx in enumerate(indices[0]):
            if idx < len(self.image_paths):  # 确保索引有效
                # FAISS返回的是内积,需要转换为余弦相似度
                # 因为embedding已经归一化,内积就是余弦相似度
                similarity = distances[0][i]
                results.append((
                    self.image_paths[idx],
                    similarity,
                    self.image_metadata[idx]
                ))
        
        return results

FAISS可以处理百万级别的向量,搜索速度极快。对于生产环境,推荐使用FAISS、Milvus、Qdrant等专业的向量数据库。

7.3 缓存机制

生成embedding是比较耗时的操作,特别是对于大图片。我们可以实现缓存机制:

import hashlib
import json
from pathlib import Path

class CachedRetrievalSystem(ImageRetrievalSystem):
    """带缓存的检索系统"""
    
    def __init__(self, model, processor, tokenizer, cache_dir=".embedding_cache"):
        super().__init__(model, processor, tokenizer)
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(exist_ok=True)
    
    def _get_cache_key(self, image_path):
        """生成缓存键:文件路径+修改时间"""
        path = Path(image_path)
        if not path.exists():
            return None
        
        # 使用文件路径和修改时间作为缓存键
        mtime = path.stat().st_mtime
        key_str = f"{path.absolute()}:{mtime}"
        return hashlib.md5(key_str.encode()).hexdigest()
    
    def add_image(self, image_path, image=None, metadata=None):
        """带缓存的添加图片"""
        # 检查缓存
        cache_key = self._get_cache_key(image_path)
        cache_file = self.cache_dir / f"{cache_key}.pkl"
        
        if cache_file.exists():
            # 从缓存加载
            with open(cache_file, 'rb') as f:
                cached_data = pickle.load(f)
            
            embedding = cached_data['embedding']
            print(f"从缓存加载: {image_path}")
        else:
            # 生成新的embedding
            if image is None:
                image = Image.open(image_path).convert("RGB")
            
            embedding = get_image_embedding(image, self.model, self.processor)
            
            # 保存到缓存
            with open(cache_file, 'wb') as f:
                pickle.dump({'embedding': embedding}, f)
            print(f"生成并缓存: {image_path}")
        
        # 添加到系统
        self.image_paths.append(image_path)
        self.image_embeddings.append(embedding)
        self.image_metadata.append(metadata or {})
        
        return True

8. 常见问题与解决方案

在实际使用中,你可能会遇到一些问题。这里总结了一些常见问题和解决方法:

8.1 显存不足问题

问题:处理大图片或批量处理时显存不足。

解决方案

  1. 使用量化模型:GLM-4v-9b支持INT4量化,显存占用从18GB降到9GB
  2. 减小批处理大小:将batch_size从4降到2或1
  3. 降低图片分辨率:虽然GLM-4v-9b支持1120×1120,但对于简单图片可以适当降低
  4. 使用CPU卸载:对于非常大的模型,可以使用accelerate库的CPU卸载功能
# 使用INT4量化
from transformers import BitsAndBytesConfig

quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

model = AutoModel.from_pretrained(
    model_name,
    quantization_config=quantization_config,  # 添加量化配置
    device_map="auto",
    trust_remote_code=True
)

8.2 相似度分数不理想

问题:某些图片和文字的相似度分数偏低,即使看起来相关。

解决方案

  1. 检查embedding归一化:确保所有embedding都进行了L2归一化
  2. 调整相似度阈值:根据实际数据调整匹配阈值
  3. 使用更详细的文字描述:提供更丰富、更准确的描述
  4. 微调模型:在自己的数据上微调模型(需要训练数据)

8.3 处理速度慢

问题:生成embedding或搜索速度慢。

解决方案

  1. 使用vLLM加速:GLM-4v-9b支持vLLM,可以大幅提升推理速度
  2. 预计算embedding:对于静态图片库,提前计算好所有embedding
  3. 使用向量数据库:对于大规模搜索,使用FAISS等专用数据库
  4. 异步处理:对于实时性要求不高的场景,使用异步队列处理

8.4 中文支持问题

问题:中文描述搜索效果不如英文。

解决方案

  1. 使用中英混合描述:GLM-4v-9b对中英文都支持良好,可以混合使用
  2. 确保tokenizer正确加载:使用trust_remote_code=True参数
  3. 检查文本编码:确保中文文本正确编码,没有乱码

9. 总结

通过本教程,你应该已经掌握了GLM-4v-9b在图文联合embedding生成和跨模态检索方面的核心用法。让我们回顾一下关键要点:

9.1 核心收获

  1. 理解了embedding的本质:embedding是内容的"数学指纹",语义相似的内容在embedding空间里距离相近。

  2. 掌握了GLM-4v-9b的核心能力:这个模型不仅能理解图片和文字,还能让它们在同一个语义空间里对齐,这是跨模态检索的基础。

  3. 学会了实际应用:从生成embedding、计算相似度,到构建完整的检索系统,你现在可以自己实现一个跨模态搜索应用了。

  4. 了解了优化技巧:批量处理、向量数据库、缓存机制等技巧能帮你处理大规模数据。

9.2 应用场景扩展

GLM-4v-9b的图文联合embedding技术可以应用在很多场景:

  • 电商平台:商品图片搜索、相似商品推荐
  • 内容管理:媒体库智能分类、自动打标签
  • 教育领域:图文教材检索、习题匹配
  • 医疗影像:病例图片与诊断报告关联
  • 安防监控:嫌疑人图片与描述匹配

9.3 下一步建议

如果你想进一步深入:

  1. 尝试不同任务:除了检索,GLM-4v-9b还支持视觉问答、图像描述等任务
  2. 优化性能:在自己的数据集上微调模型,提升特定领域的准确率
  3. 构建完整应用:将检索系统集成到Web应用或移动App中
  4. 探索多模态RAG:结合检索增强生成,构建更智能的多模态问答系统

9.4 最后的话

多模态AI正在改变我们处理和理解信息的方式。GLM-4v-9b作为一个开源的高性能多模态模型,让开发者能够以较低的成本构建强大的跨模态应用。

记住,技术本身不是目的,解决实际问题才是。希望本教程能帮你快速上手,将GLM-4v-9b的能力应用到你的项目中。如果在实践中遇到问题,多查阅官方文档,多在社区交流,技术之路就是不断尝试和解决问题的过程。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐