第二课:多模态数据基础

各位同学,今天我们来聊聊多模态系统里最基础、也最绕不开的话题——数据表示。说白了,你让机器理解图像、文字、声音,总得先给它一种它能“消化”的格式。我刚开始接触多模态时,以为就是把图片和文字拼在一起喂给模型就行,结果踩了不少坑。嗯,这节课我们就把这些坑一个个填上。

一、图像数据表示:从像素到张量

图像在计算机眼里是什么?不是我们看到的风景或人脸,而是一堆数字。最常见的格式就是RGB。

RGB 色彩模型:每个像素由红、绿、蓝三个通道组成,每个通道取值0-255。一张 224x224 的彩色图片,其实就是三个 224x224 的矩阵叠在一起。

核心概念:图像张量的形状通常是 (H, W, C) 或 (C, H, W)。H是高度,W是宽度,C是通道数。RGB图像C=3,灰度图C=1。

我个人习惯用 PyTorch 的格式 (C, H, W),因为卷积操作默认走这个顺序。但 OpenCV 读出来是 (H, W, C),所以每次都要做一次维度转换。我在项目中遇到过因为忘记转维度,模型训练直接报错的情况——嗯,这种低级错误犯过一次就记住了。

import numpy as np
from PIL import Image

# 读取图像,得到 (H, W, C) 格式
img = np.array(Image.open('cat.jpg'))
print(img.shape)  # 比如 (224, 224, 3)

# 转成 (C, H, W) 格式
img_tensor = img.transpose(2, 0, 1)
print(img_tensor.shape)  # (3, 224, 224)

为什么要转成张量?因为深度学习模型只认张量——说白了就是多维数组。图像张量还需要归一化,把0-255的像素值缩放到0-1或-1到1之间。我常用的归一化方式是除以255,然后减去均值除以标准差。

小技巧:归一化时记得用 ImageNet 的均值和标准差(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),这是业界惯例,迁移学习效果更好。

二、文本数据表示:从 Token 到 Embedding

文本怎么变成数字?你想想看,模型不认识汉字,它只认向量。所以我们需要两步走:先分词(Tokenization),再映射成向量(Embedding)。

Token 是文本的最小单位。中文里,一个词或一个字都可以是一个 Token。比如“我爱北京天安门”,分词后可能是 ["我", "爱", "北京", "天安门"]。每个 Token 对应一个唯一的整数 ID。

Embedding 就是把 Token ID 映射成一个稠密向量。比如“北京”这个词,可能对应一个 768 维的向量。这个向量里包含了语义信息——相似的词在向量空间里距离更近。

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
text = "我爱北京天安门"
tokens = tokenizer.tokenize(text)
print(tokens)  # ['我', '爱', '北', '京', '天', '安', '门']

ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)  # [2769, 4263, 1266, 776, 1921, 2128, 7305]

我曾经在做一个中文问答系统时,直接用字级别分词,结果模型对“北京”和“北 京”的理解完全不一样。后来改用词级别分词,效果提升明显。这里要注意:分词粒度直接影响模型表现,没有绝对的好坏,得看任务。

避坑指南:我曾经因为忘记添加特殊 Token(如 [CLS]、[SEP]),导致 BERT 模型输出结果完全不对。这些特殊 Token 是模型预训练时就设计好的,不能省。

三、音频数据表示:从波形到频谱

音频数据比图像和文本更“原始”。麦克风采集到的是一维的波形信号——说白了就是声压随时间的变化。但直接把波形喂给模型,效果通常不好。为什么?因为人耳对频率更敏感,而不是振幅。

所以我们需要把波形转成频谱图(Spectrogram)。常用的方法是短时傅里叶变换(STFT),把音频切成一小段一小段,每段做傅里叶变换,得到频率分布。最后拼成一张二维图——横轴是时间,纵轴是频率,颜色深浅代表能量大小。

import librosa
import numpy as np

# 读取音频,得到波形
audio, sr = librosa.load('speech.wav', sr=16000)
print(audio.shape)  # (采样点数,)

# 计算梅尔频谱
mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr, n_mels=80)
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
print(mel_spec_db.shape)  # (80, 时间帧数)

我个人习惯用梅尔频谱(Mel Spectrogram),因为它模拟了人耳的听觉特性——对低频更敏感,高频分辨率低一些。80维的梅尔频谱是语音领域的标配,就像图像里的224x224一样。

关键点:音频数据预处理通常包括:重采样(统一采样率)、静音切除、归一化、提取特征。每一步都可能影响最终效果。

四、数据预处理对齐:多模态的“配钥匙”

现在我们有图像张量、文本 Embedding、音频频谱——但它们的时间尺度和维度都不一样。怎么对齐?这是多模态系统最头疼的问题。

举个例子:一段视频里,图像是每秒30帧,音频是每秒16000个采样点,文本是每几秒一个词。你想想看,模型怎么同时处理这三种不同节奏的数据?

常见的对齐策略有三种:

  1. 时间戳对齐:给每个模态的数据打上时间戳,按时间对齐。比如视频的第2.5秒对应音频的第40000个采样点。
  2. 特征维度对齐:通过线性投影或 MLP,把不同模态的特征映射到相同的维度空间。比如图像特征768维,文本特征也映射到768维。
  3. 序列长度对齐:通过采样或插值,让不同模态的序列长度一致。比如把30帧的图像序列下采样到10帧,匹配文本的10个词。
# 示例:将图像特征和文本特征映射到同一空间
import torch.nn as nn

image_feat_dim = 2048
text_feat_dim = 768
common_dim = 512

image_proj = nn.Linear(image_feat_dim, common_dim)
text_proj = nn.Linear(text_feat_dim, common_dim)

# 对齐后的特征
image_out = image_proj(image_features)  # (batch, 512)
text_out = text_proj(text_features)     # (batch, 512)

我在做一个视频理解项目时,视频帧率是25fps,但音频特征每10ms一帧。对齐时我选择了按音频帧率采样视频帧,因为音频的时间分辨率更高,丢失的信息更少。嗯,这个选择后来被证明是正确的。

经验之谈:对齐时不要盲目统一采样率。先想清楚哪个模态是“主时钟”,其他模态去对齐它。通常音频或视频帧率较高的那个作为基准。

五、知识体系总览

下面这张图概括了本章的核心内容。我把它画成了流程图,方便你理解数据从原始形态到模型输入的完整链路。

多模态数据表示与对齐流程 图像 (RGB像素) 归一化 → 张量 (C,H,W) 文本 (字符串) 分词 → Token ID → Embedding 音频 (波形) STFT → 梅尔频谱 数据对齐 时间戳对齐 / 维度映射 / 序列采样 统一多模态特征 (共同空间) 三种模态经过各自预处理后,在对齐模块中统一到相同的时间/维度空间 最终得到可供多模态模型直接使用的联合特征表示

从这张图可以看得很清楚:图像、文本、音频各有各的“方言”,但最终都要翻译成模型能听懂的“普通话”——也就是统一维度的特征向量。对齐这一步做不好,后面模型再强也白搭。

重要提醒:我曾经因为图像和文本的 batch size 不一致,导致训练时维度匹配出错。多模态数据加载时,务必保证每个 batch 里所有模态的样本数相同,且一一对应。

好了,这节课的内容就是这些。数据表示是地基,地基不稳,上层建筑再漂亮也容易塌。下一节我们会深入具体的多模态模型架构,到时候这些预处理的知识都会用上。


公众号:蓝海资料掘金营,微信deep3321