3、多模态表示学习:图像、文本、音频的嵌入表示,模态对齐与共享语义空间

好,咱们进入第三个话题。多模态表示学习。

说实话,这是整个多模态融合里最核心、也最头疼的一块。你想想看,图像是一堆像素,文本是一串字符,音频是一段波形——这三者根本就不是一个世界的语言。怎么让它们互相理解?

答案就是:把它们都映射到同一个“语义空间”里。

3.1 各模态的嵌入表示

先说说每个模态怎么变成向量。我习惯把这一步叫做“翻译”——把原始信号翻译成机器能懂的数学语言。

3.4.1 图像嵌入

图像嵌入,说白了就是把一张图压缩成一个固定长度的向量。早期我用CNN(卷积神经网络)做这事,比如ResNet-50,把最后一层全连接层的输出拿过来,就是一个2048维的向量。

但现在我更推荐用Vision Transformer(ViT)。为什么?因为ViT能把图像切成patch,然后用Transformer的注意力机制去捕捉全局关系。我在做商品检索项目时,ViT对细粒度特征的保留明显好于CNN。

# 伪代码:图像嵌入
import torchvision.models as models
model = models.resnet50(pretrained=True)
# 去掉最后的分类层
embedding = model.avgpool(model.layer4(features)).squeeze()
# embedding shape: [2048]
小技巧: 如果你做的是细粒度分类(比如区分不同品种的狗),建议用ViT-B/16,patch size小一点,细节保留更好。

3.4.2 文本嵌入

文本嵌入,大家应该很熟了。从Word2Vec到BERT,再到现在的GPT系列,路子越来越野。

我个人习惯用BERT的[CLS] token作为整句的表示。为什么?因为BERT在预训练时,[CLS] token就是被设计来做分类任务的,它天然包含了整个句子的语义信息。

不过要注意一点:BERT的输入长度有限制(通常是512个token)。我在处理长文档时,曾经踩过这个坑——直接截断导致丢失了关键信息。后来我改用Longformer或者分段处理再取平均。

# 伪代码:文本嵌入
from transformers import BertModel, BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
inputs = tokenizer("一只猫在沙发上睡觉", return_tensors="pt")
outputs = model(**inputs)
# 取[CLS] token
embedding = outputs.last_hidden_state[:, 0, :]  # shape: [1, 768]

3.4.3 音频嵌入

音频嵌入,嗯,这个稍微冷门一点。但如果你做视频理解或者语音交互,就绕不开它。

我常用的做法是:先把音频转成梅尔频谱图(Mel-spectrogram),然后用类似图像的方法去处理。说白了,就是把声音“画”出来,再用CNN或者ViT去提取特征。

还有一个选择是直接用Wav2Vec 2.0这样的预训练模型,它直接在原始波形上做自监督学习,效果很不错。我在做语音情感识别时,Wav2Vec 2.0比传统方法高了将近5个点。

# 伪代码:音频嵌入(梅尔频谱+CNN)
import torchaudio
waveform, sample_rate = torchaudio.load('audio.wav')
mel_spec = torchaudio.transforms.MelSpectrogram()(waveform)
# 然后用CNN提取特征
# embedding shape: [512]

3.2 模态对齐

好,现在每个模态都有自己的向量了。但问题来了:图像里的“猫”和文本里的“猫”,在向量空间里离得近吗?

不一定。因为它们是分别训练的,各自在自己的空间里。模态对齐,就是要让它们“对上号”。

3.2.1 对比学习对齐

这是目前最主流的方法。代表作就是CLIP(Contrastive Language-Image Pre-training)。

思路很简单:把配对的图像-文本对拉近,把不配对的推远。我当年第一次看到CLIP的效果时,确实被震撼到了——它居然能零样本做分类。

# 伪代码:对比学习损失(InfoNCE)
def contrastive_loss(image_emb, text_emb, temperature=0.07):
    # image_emb, text_emb: [batch_size, dim]
    logits = torch.matmul(image_emb, text_emb.T) / temperature
    labels = torch.arange(batch_size)  # 对角线是正样本
    loss_i = cross_entropy(logits, labels)
    loss_t = cross_entropy(logits.T, labels)
    return (loss_i + loss_t) / 2
避坑指南: 我曾经在batch size设得太小时,对比学习效果很差。因为负样本太少,模型学不到区分度。建议batch size至少256以上。

3.2.2 跨模态注意力对齐

除了对比学习,还有一种更精细的对齐方式——用注意力机制让两个模态互相“看”对方。

比如在视觉问答任务中,模型需要根据问题去关注图像的特定区域。这就是跨模态注意力在做的事。

# 伪代码:跨模态注意力
# 图像特征: img_feat [num_patches, dim]
# 文本特征: txt_feat [seq_len, dim]
attn_scores = torch.matmul(txt_feat, img_feat.T)  # [seq_len, num_patches]
attn_weights = torch.softmax(attn_scores, dim=-1)
aligned_feat = torch.matmul(attn_weights, img_feat)  # 文本关注的图像区域

3.3 共享语义空间

模态对齐的终极目标,就是构建一个共享语义空间。在这个空间里,“猫”的图像、“猫”的文字、“猫”的叫声,它们的向量应该挨在一起。

3.3.1 统一嵌入空间

我见过两种做法:

  • 单塔结构:所有模态共享一个编码器。比如ImageBERT,把图像patch和文本token拼在一起,用一个Transformer处理。好处是交互充分,缺点是计算量大。
  • 双塔结构:每个模态有自己的编码器,但通过损失函数让它们的输出空间对齐。CLIP就是典型。好处是高效,可以独立编码,缺点是交互不够深。
方法 优点 缺点 适用场景
单塔 交互充分,对齐精度高 计算量大,推理慢 需要细粒度对齐的任务(如视觉推理)
双塔 高效,可独立编码 交互不足,对齐粗糙 检索、分类等粗粒度任务

3.3.2 多任务学习增强语义空间

我个人经验是:只用对比学习往往不够。因为对比学习只关心“配不配对”,不关心具体的语义内容。

我建议加上一些辅助任务,比如:

  • 掩码重建:遮住图像的一部分,让模型根据文本去预测被遮住的内容。
  • 跨模态翻译:给定图像,生成对应的文本描述(image captioning)。
  • 模态分类:让模型判断两个输入是否属于同一个语义类别。
核心观点: 共享语义空间不是一蹴而就的。它需要对比学习拉近距离,需要注意力机制精细对齐,还需要多任务学习丰富语义。三者缺一不可。

3.4 实践中的坑与建议

最后,分享几个我在项目中踩过的坑:

  1. 模态不平衡:文本模态往往比图像模态更容易过拟合。我建议给文本模态加更大的dropout,或者用更小的学习率。
  2. 维度选择:不同模态的嵌入维度最好一致。我见过有人图像用2048维,文本用768维,结果对齐时怎么调都不对。统一成768或1024会省心很多。
  3. 温度系数:对比学习里的temperature参数很敏感。我习惯从0.07开始调,如果发现正样本分数太集中就调大,太分散就调小。

嗯,多模态表示学习这块,说白了就是“翻译+对齐”。把不同语言翻译成同一种向量语言,然后让它们互相理解。下一章我们会聊到具体的融合策略——怎么把这些对齐好的向量真正用起来。