特征表示基础:单模态特征提取与度量学习

各位同学,咱们今天聊点实在的。跨模态对齐,说白了就是让图像和文本在同一个"语义空间"里对话。但在这之前,你得先让每种模态自己把话说清楚。这就是单模态特征提取要做的事。

我刚开始做多模态检索那会儿,踩过一个坑——直接拿预训练模型的特征去对齐,结果发现图像特征和文本特征根本不在一个量级上。后来才明白,特征表示的质量,直接决定了后续对齐的天花板。

图像特征提取:从CNN到现代架构

图像特征提取,最经典的就是CNN。你想想看,一张224x224的图片,经过卷积、池化、激活,最后变成一个2048维的向量。这个向量,就是图像的"身份证"。

核心思路: 用卷积核扫描图像,逐层抽象。底层学边缘、纹理,高层学物体、场景。

我个人习惯用ResNet-50作为基线模型。为什么?因为它引入了残差连接,解决了深层网络退化问题。代码实现也很干净:

import torchvision.models as models
import torch

# 加载预训练ResNet-50,去掉最后的全连接层
resnet = models.resnet50(pretrained=True)
feature_extractor = torch.nn.Sequential(*list(resnet.children())[:-1])

# 输入: [batch, 3, 224, 224] -> 输出: [batch, 2048, 1, 1]
img_tensor = torch.randn(4, 3, 224, 224)
img_feat = feature_extractor(img_tensor).squeeze()
print(img_feat.shape)  # torch.Size([4, 2048])

这里要注意,squeeze() 之后得到的是2048维的全局特征。但我在项目中遇到过一个问题——全局特征丢失了空间信息。比如"左边的狗"和"右边的猫",全局特征根本区分不了位置。

避坑指南: 我曾经在图文匹配任务里直接用全局特征,结果模型把"红车在左边"和"红车在右边"当成一样的。后来改用区域特征(R-CNN系列)或者注意力机制,才解决这个问题。

除了ResNet,现在更常用的是Vision Transformer(ViT)。它把图像切成16x16的patch,然后用Transformer处理。效果确实好,但计算量也大。我的建议是:

  • 小数据集(万级):用ResNet-50/101,稳定且不易过拟合
  • 大数据集(百万级):用ViT或Swin Transformer,精度更高
  • 移动端部署:用MobileNet或EfficientNet-Lite

文本特征提取:从RNN到BERT

文本这边,传统做法是RNN/LSTM。但说实话,RNN有个硬伤——长距离依赖。你想想看,一句话50个词,第一个词和最后一个词的信息传递,要经过49步,梯度早就消失了。

我早期做文本检索时,用Bi-LSTM提取句子特征。前向和后向的隐藏状态拼接,得到整个句子的表示。代码大概长这样:

import torch.nn as nn

class BiLSTMEncoder(nn.Module):
    def __init__(self, vocab_size, embed_dim=300, hidden_dim=256):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim)
        self.lstm = nn.LSTM(embed_dim, hidden_dim, 
                           bidirectional=True, batch_first=True)
    
    def forward(self, x):
        # x: [batch, seq_len]
        emb = self.embedding(x)  # [batch, seq_len, 300]
        output, (h_n, c_n) = self.lstm(emb)
        # 取最后一个时间步的隐藏状态
        last_hidden = h_n[-2:, :, :].transpose(0, 1).reshape(x.size(0), -1)
        return last_hidden  # [batch, 512]

但后来BERT出来之后,RNN基本就被取代了。为什么?因为BERT用了Transformer的Self-Attention,每个词都能直接看到所有词,不存在长距离问题。

经验之谈: 我建议文本特征提取直接用BERT或其变体(RoBERTa、ALBERT)。如果算力有限,可以用DistilBERT,速度提升40%,精度只降2-3%。

用BERT提取特征,通常取[CLS] 的输出作为句子表示:

from transformers import BertTokenizer, BertModel

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

text = "A dog is playing in the garden"
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True)
outputs = model(**inputs)

# 取[CLS] 的特征
text_feat = outputs.last_hidden_state[:, 0, :]  # [1, 768]

特征空间与度量学习基础

好,现在图像和文本各自的特征都提取出来了。但问题来了——图像特征是2048维,文本特征是768维,怎么对齐?

这就是特征空间要做的事。我们需要一个公共空间,把不同模态的特征映射进去。在这个空间里,相似的图文对距离近,不相似的距离远。

核心思想: 学习一个映射函数 f(·) 和 g(·),使得 f(图像) 和 g(文本) 在公共空间中的距离,反映它们的语义相似度。

度量学习就是用来学这个距离的。常用的损失函数有几种:

损失函数 核心思想 适用场景
Contrastive Loss 正样本对拉近,负样本对推远,固定margin 成对数据,二分类式匹配
Triplet Loss 锚点与正样本距离 < 锚点与负样本距离 + margin 三元组数据,细粒度检索
InfoNCE Loss 将正样本从N个负样本中区分出来,基于softmax 大规模对比学习(CLIP、SimCLR)
Margin Ranking Loss 直接优化正样本对和负样本对的得分差值 排序任务,推荐系统

我个人最常用的是InfoNCE Loss。为什么?因为它天然适合大规模数据。你想想看,一个batch里N个图文对,每个图文对是正样本,其他N-1个都是负样本。这样一次前向就能学到很强的判别力。

代码实现也不复杂:

import torch.nn.functional as F

def info_nce_loss(image_features, text_features, temperature=0.07):
    """
    image_features: [batch, dim]
    text_features: [batch, dim]
    """
    # 归一化
    image_features = F.normalize(image_features, dim=-1)
    text_features = F.normalize(text_features, dim=-1)
    
    # 计算相似度矩阵 [batch, batch]
    logits = image_features @ text_features.T / temperature
    
    # 对角线是正样本对
    batch_size = logits.shape[0]
    labels = torch.arange(batch_size).to(logits.device)
    
    # 双向损失
    loss_i2t = F.cross_entropy(logits, labels)
    loss_t2i = F.cross_entropy(logits.T, labels)
    
    return (loss_i2t + loss_t2i) / 2
注意: temperature参数很关键。我调参时发现,temperature太大(>0.1)会让所有样本的相似度趋同,太小(<0.01)会让模型只关注最难样本。一般0.05-0.07是个好起点。

本章知识体系总览

下面这张图,把本章的核心逻辑串起来了。从原始数据到特征提取,再到公共空间对齐,每一步都有对应的技术和注意事项。

单模态特征提取与度量学习知识体系 图像输入 CNN / ViT 特征提取 2048维 / 768维特征 文本输入 RNN / BERT 特征提取 512维 / 768维特征 公共特征空间 映射函数 f(·) 和 g(·) 度量学习损失函数 Contrastive / Triplet InfoNCE / Margin Ranking 对齐后的跨模态特征

从这张图你能看到,整个流程是串行的:原始数据 → 单模态特征提取 → 映射到公共空间 → 度量学习优化。每一步都有多种选择,具体用哪个,取决于你的数据规模、算力资源和业务场景。

我的建议: 刚开始做跨模态项目,别一上来就上最复杂的模型。先用ResNet+BERT+InfoNCE搭一个基线,跑通流程。然后再逐步替换组件,看哪个环节收益最大。我在多个项目里验证过,这个策略最省时间。