3、多模态表示学习:单模态编码器与共享语义空间
好,咱们进入第三章。这一章,我个人觉得是整个多模态对齐的基石。
说白了,你要让不同模态的数据“对话”,首先得让它们各自“学会说话”。
今天我们就来聊聊三个最常用的单模态编码器:文本的BERT、图像的ViT、音频的Wav2Vec。然后,我们再聊聊那个神奇的“共享语义空间”到底是个啥。
3.1 文本编码器:BERT 的“双向”理解
先说说文本。BERT,全称是 Bidirectional Encoder Representations from Transformers。名字很长,但核心就一个词:双向。
什么意思?
以前的模型看一句话,是从左往右看,或者从右往左看。但BERT不一样,它同时看左右两边。比如“我今天去银行存钱”,它看“银行”这个词的时候,会同时看左边的“我今天去”和右边的“存钱”。这样它就知道,这里的“银行”是金融机构,不是河岸。
我个人习惯把BERT看作一个“阅读理解”高手。它不生成文本,它只负责把文本变成一串有意义的向量。
核心要点: BERT 的输出是每个 token 对应的向量表示。我们通常取 [CLS] 标签对应的向量作为整个句子的语义表示。
我在项目中遇到过一个问题:直接用BERT处理长文本,显存直接爆掉。后来我学乖了,一般会做截断或者分段处理。BERT的输入长度上限是512个token,超过这个数,就得想办法了。
# 一个简单的BERT使用示例
from transformers import BertTokenizer, BertModel
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
text = "我今天去银行存钱"
inputs = tokenizer(text, return_tensors="pt")
outputs = model(**inputs)
# 取 [CLS] 向量作为句子表示
sentence_embedding = outputs.last_hidden_state[:, 0, :]
print(sentence_embedding.shape) # torch.Size([1, 768])
3.2 图像编码器:ViT 的“图块”革命
图像这边,ViT(Vision Transformer)是个颠覆性的东西。
以前做图像,大家用CNN,一层层卷积,提取特征。ViT呢?它直接把图像切成一个个小方块(patch),然后把这些小方块当成文本里的“单词”来处理。
你想想看,一张224x224的图片,切成16x16的小块,一共196个块。每个块拉平成一个向量,再加上位置编码,扔进Transformer里。就这么简单粗暴。
我的经验: ViT 在小数据集上容易过拟合。我曾经在一个只有几千张图片的项目里用ViT,效果还不如ResNet。后来用了预训练权重,才把效果拉回来。所以,数据量不够,别硬上ViT。
ViT的输出是什么?每个patch对应一个向量。和BERT类似,我们通常会在输入序列前面加一个 [CLS] 标签,用它对应的向量代表整张图片。
# ViT 使用示例
from transformers import ViTImageProcessor, ViTModel
from PIL import Image
processor = ViTImageProcessor.from_pretrained('google/vit-base-patch16-224')
model = ViTModel.from_pretrained('google/vit-base-patch16-224')
image = Image.open("example.jpg")
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)
# 取 [CLS] 向量作为图像表示
image_embedding = outputs.last_hidden_state[:, 0, :]
print(image_embedding.shape) # torch.Size([1, 768])
3.3 音频编码器:Wav2Vec 的“自监督”魔法
音频这块,Wav2Vec 2.0 是我目前用得最多的。
音频数据和文本、图像不太一样。它是连续的波形信号,没有天然的分词。Wav2Vec的做法是:先把原始音频波形用CNN编码成一个个“帧”,然后对这些帧做掩码,再用Transformer去预测被掩码的部分。
嗯,这里要注意:Wav2Vec 的输入是原始波形,不需要你做MFCC之类的特征提取。它自己就能学出好的表示。
避坑指南: 我曾经在Wav2Vec上踩过一个坑——采样率不匹配。Wav2Vec预训练时用的是16kHz的音频,如果你拿48kHz的音频直接喂进去,效果会非常差。记得先做重采样。
# Wav2Vec 使用示例
from transformers import Wav2Vec2Processor, Wav2Vec2Model
import torchaudio
processor = Wav2Vec2Processor.from_pretrained('facebook/wav2vec2-base-960h')
model = Wav2Vec2Model.from_pretrained('facebook/wav2vec2-base-960h')
waveform, sample_rate = torchaudio.load("example.wav")
# 重采样到16kHz
if sample_rate != 16000:
resampler = torchaudio.transforms.Resample(sample_rate, 16000)
waveform = resampler(waveform)
inputs = processor(waveform, sampling_rate=16000, return_tensors="pt")
outputs = model(**inputs)
# 取平均池化作为音频表示
audio_embedding = outputs.last_hidden_state.mean(dim=1)
print(audio_embedding.shape) # torch.Size([1, 768])
3.4 共享语义空间:让“猫”和“喵”对齐
好了,三个编码器都讲完了。现在问题来了:文本里的“猫”、图片里的猫、音频里的“喵喵叫”,它们怎么对应起来?
答案就是——共享语义空间。
说白了,我们让三个编码器输出的向量,都映射到同一个高维空间里。在这个空间里,“猫”的文本向量、图片向量、音频向量,应该离得很近。
核心思想: 共享语义空间不是天然存在的,而是通过对比学习等训练方法“逼”出来的。我们让匹配的多模态对在空间里靠近,不匹配的远离。
怎么训练?最经典的方法是CLIP的做法:
- 拿一批图文对,比如“一只猫在睡觉”和对应的图片
- 分别用文本编码器和图像编码器得到向量
- 计算所有文本向量和所有图像向量之间的相似度(比如余弦相似度)
- 让匹配的对相似度尽量高,不匹配的对相似度尽量低
我个人的习惯是,在训练初期先用一个较小的学习率,让三个编码器慢慢适应这个共享空间。如果一开始学习率太大,模型容易“学歪”,把不相关的概念强行对齐。
| 模态 | 编码器 | 输出维度 | 典型应用 |
|---|---|---|---|
| 文本 | BERT | 768 | 文本分类、语义搜索 |
| 图像 | ViT | 768 | 图像分类、目标检测 |
| 音频 | Wav2Vec | 768 | 语音识别、情感分析 |
你可能会问:为什么三个编码器的输出维度都是768?
嗯,这不是巧合。很多预训练模型都用了768维,方便做跨模态对齐。当然,也有用1024维的,比如BERT-large。但维度越高,计算量越大,训练也越难。
我的建议: 如果你刚开始做多模态项目,先用768维的模型。等把流程跑通了,再考虑换更大的模型。我曾经一上来就用1024维的模型,结果训练速度慢得让人崩溃,调试起来也麻烦。
最后,总结一下这一章的核心:
- BERT把文本变成向量,ViT把图像变成向量,Wav2Vec把音频变成向量
- 这三个向量都落在同一个高维空间里,就是共享语义空间
- 共享语义空间是通过对比学习训练出来的,不是天然存在的
- 实际应用中要注意输入格式、采样率、序列长度等细节
下一章,我们会深入对比学习的细节,看看怎么让这些向量真正“对齐”。到时候我会分享一些我在实际项目中踩过的坑,保证有用。