向量化基础:Embedding 技术原理与多模态实践

好,咱们正式开始动手之前,先把地基打牢。向量化,说白了就是把文字、图片、声音这些人类能理解的东西,变成机器能算的数学向量。你想想看,计算机不认识「猫」这个字,但它能算 [0.23, -0.45, 0.78, ...] 这个向量跟另一个向量的距离。这就是 Embedding 的核心——语义映射到几何空间。

核心认知:Embedding 不是加密,是压缩与抽象。它把高维的离散信息(比如一个词、一张图)映射到低维连续向量空间,让语义相近的东西在空间里靠得近。

2.1 Embedding 技术原理:从 One-Hot 到分布式表示

最早的做法是 One-Hot 编码。假设词表有 10 万个词,每个词就是一个 10 万维的向量,只有自己那一位是 1,其他全是 0。这有什么问题?维度爆炸、语义孤岛。「苹果」和「香蕉」在 One-Hot 空间里距离跟「苹果」和「汽车」一样远,完全没体现语义关系。

分布式表示(Distributed Representation)解决了这个问题。每个词用一个稠密向量表示,维度通常是 128、256 或 768。向量的每一维都不再是 0/1,而是浮点数,共同编码了语义特征。我习惯把这种向量叫做「语义指纹」——每个实体都有自己独特的数值模式。

我的经验:刚开始做检索系统时,我试过用 One-Hot 加 TF-IDF 做文本匹配,效果惨不忍睹。后来换成 300 维的 GloVe 向量,召回率直接翻了一倍。维度不是越高越好,关键看训练数据够不够、任务合不合适。

现代 Embedding 的核心训练逻辑是:上下文预测。不管是 Word2Vec 的 CBOW/Skip-gram,还是 BERT 的 Masked Language Model,本质上都是让模型根据上下文猜当前词(或反过来)。猜对了,向量就调得更准。最终,语义相似的词在向量空间里聚在一起。

这里有个关键概念——嵌入矩阵。你可以把它理解成一个查找表:输入一个词 ID,查表拿到对应的向量。这个矩阵就是模型学到的全部「词汇知识」。多模态场景下,不同模态有各自的嵌入矩阵,但最终要映射到同一个对齐空间。

2.2 文本向量化:BERT 与 CLIP 文本编码器

文本向量化是咱们用得最多的。我主要讲两个主流方案:BERT 和 CLIP 的文本编码器。

2.2.1 BERT:双向上下文感知

BERT 跟传统词向量的最大区别是:动态上下文。传统词向量(如 Word2Vec)给每个词一个固定向量,不管它出现在什么语境里。「苹果」在「吃苹果」和「苹果手机」里是同一个向量。BERT 会根据整句话生成每个词的向量,所以「苹果」在不同句子里向量不同。

用 BERT 做向量化,我建议取 [CLS] 的最后一层输出作为整句向量。代码很简单:

from transformers import BertTokenizer, BertModel
import torch

tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')

text = "多模态检索系统需要处理文本和图像"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
outputs = model(**inputs)

# 取 [CLS] 向量作为句子表示
sentence_embedding = outputs.last_hidden_state[:, 0, :]
print(sentence_embedding.shape)  # torch.Size([1, 768])

避坑指南:我曾经直接拿 BERT 的原始输出做检索,发现相似度分数普遍偏高,区分度不够。后来加了 L2 归一化,效果明显改善。另外,BERT 的向量维度是 768,如果后续要存到向量数据库,记得考虑存储成本。

2.2.2 CLIP 文本编码器:跨模态对齐

CLIP 的文本编码器跟 BERT 结构类似,但训练目标不同。CLIP 用 4 亿图文对训练,让文本向量和对应图像向量在空间里靠近。所以 CLIP 的文本向量天然适合做图文检索。

我个人习惯用 /clip-vit-base-patch32 这个版本,输出是 512 维。相比 BERT 的 768 维,维度更低,检索速度更快,而且跨模态对齐效果更好。

from transformers import CLIPTokenizer, CLIPTextModel

tokenizer = CLIPTokenizer.from_pretrained('/clip-vit-base-patch32')
text_model = CLIPTextModel.from_pretrained('/clip-vit-base-patch32')

text = "一只橘猫坐在窗台上"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
outputs = text_model(**inputs)

# CLIP 文本向量
text_emb = outputs.pooler_output
print(text_emb.shape)  # torch.Size([1, 512])
模型 维度 特点 适用场景
BERT-base 768 上下文感知,语义丰富 纯文本检索、语义匹配
CLIP 文本编码器 512 跨模态对齐,维度低 图文检索、多模态检索
Sentence-BERT 768 句向量优化,对比学习 句子相似度、聚类

2.3 图像向量化:ResNet 与 ViT

图像向量化就是把一张图变成向量。传统做法用 CNN,现在 ViT 也越来越主流。

2.3.1 ResNet:残差网络的经典方案

ResNet 的核心贡献是残差连接,解决了深层网络退化问题。做图像向量化时,我通常去掉最后的全连接分类层,取全局平均池化后的特征向量。

ResNet-50 输出 2048 维向量,信息量很足。但维度高也意味着存储和检索开销大。我在一个电商项目中用过 ResNet-101,检索 100 万张图,单次查询要 50 毫秒,后来降维到 256 维,速度提升到 8 毫秒,精度只掉了 2%。

import torch
import torchvision.models as models
from torchvision import transforms
from PIL import Image

model = models.resnet50(pretrained=True)
# 去掉分类层
model = torch.nn.Sequential(*list(model.children())[:-1])
model.eval()

transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

img = Image.open('cat.jpg')
img_tensor = transform(img).unsqueeze(0)

with torch.no_grad():
    img_emb = model(img_tensor).squeeze()
print(img_emb.shape)  # torch.Size([2048])

2.3.2 ViT:Transformer 征服视觉

ViT 把图像切成 16x16 的 patch,每个 patch 当成一个 ,然后用 Transformer 处理。效果在大型数据集上往往超过 ResNet。ViT-Base 输出 768 维向量,跟 BERT 维度一致,方便做跨模态融合。

我记得第一次用 ViT 时,发现它对图像尺寸很敏感。ResNet 可以接受任意尺寸(内部会自适应池化),但 ViT 要求输入尺寸固定,因为 patch 位置编码是写死的。所以用 ViT 时一定要确保预处理尺寸跟训练时一致。

选型建议:数据量小(<10万)用 ResNet,稳定可靠;数据量大(>100万)用 ViT,上限更高。如果要做跨模态对齐,优先考虑 ViT,因为 Transformer 结构跟文本模型更统一。

2.4 音频向量化:Wav2Vec 与 HuBERT

音频向量化相对小众,但在语音搜索、声纹识别场景里很关键。Wav2Vec 是 Facebook 提出的自监督模型,直接对原始波形建模,不需要人工标注。

Wav2Vec 2.0 的流程是:先用量化器把音频特征离散化,然后用 Transformer 做上下文建模。最终输出的向量可以用于语音识别、情感分析等任务。做检索时,我通常取最后一层所有时间步的平均向量作为整段音频的表示。

from transformers import Wav2Vec2Processor, Wav2Vec2Model
import torchaudio
import torch

processor = Wav2Vec2Processor.from_pretrained('facebook/wav2vec2-base-960h')
model = Wav2Vec2Model.from_pretrained('facebook/wav2vec2-base-960h')

waveform, sample_rate = torchaudio.load('speech.wav')
# 重采样到 16kHz
if sample_rate != 16000:
    resampler = torchaudio.transforms.Resample(sample_rate, 16000)
    waveform = resampler(waveform)

inputs = processor(waveform.squeeze(), sampling_rate=16000, return_tensors='pt')
with torch.no_grad():
    outputs = model(**inputs)

# 取时间维度的平均作为音频向量
audio_emb = outputs.last_hidden_state.mean(dim=1)
print(audio_emb.shape)  # torch.Size([1, 768])

我的经验:Wav2Vec 对音频长度有限制,超过 30 秒的音频建议分段处理。另外,音频向量化前一定要做 VAD(语音活动检测),把静音段去掉,否则静音部分的向量会拉低整体质量。

2.5 多模态向量统一:对齐与融合

到这里,文本、图像、音频都能变成向量了。但问题是:它们的向量空间不一样。BERT 的 768 维空间、ResNet 的 2048 维空间、Wav2Vec 的 768 维空间,彼此不互通。你不能直接用文本向量去检索图像。

解决方案有两种:

  • 统一映射:用额外的映射层(比如一个线性层 + LayerNorm)把各模态向量映射到同一个维度空间。CLIP 就是这么做的——文本和图像分别编码,然后用对比学习拉近。
  • 共享编码器:用同一个 Transformer 处理所有模态。比如 ImageBind 模型,用图像作为锚点,把文本、音频、视频都对齐到图像空间。

我个人更推荐第一种方案,灵活性高。你可以单独优化每个模态的编码器,最后用映射层统一维度。后面章节我们会详细讲怎么训练这个映射层。

下面这张图展示了多模态向量化的整体流程:

多模态向量化统一流程 文本输入 图像输入 音频输入 BERT / CLIP 文本编码器 ResNet / ViT 图像编码器 Wav2Vec 音频编码器 文本向量 (768/512维) 图像向量 (2048/768维) 音频向量 (768维) 统一映射层(线性投影 + 归一化) 统一向量空间(同维度、可检索) 说明:各模态经独立编码器生成向量后,通过统一映射层对齐到同一空间,实现跨模态检索

2.6 本章小结

向量化是整个多模态检索系统的起点。记住几个关键点:

  • Embedding 的本质是把离散信息映射到连续语义空间
  • 文本向量化用 BERT(768维)或 CLIP 文本编码器(512维),注意归一化
  • 图像向量化用 ResNet(2048维)或 ViT(768维),数据量大优先 ViT
  • 音频向量化用 Wav2Vec(768维),记得做 VAD 和分段
  • 多模态统一需要映射层对齐向量空间,不能直接混用