Transformers 中的 PeVideo:面向零样本视频分类与视频-文本检索的对比学习视频编码器实战指南
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
PE Video(Perception Encoder Video,感知编码器的视频分支)是 Meta Perception Encoder 家族中负责处理视频模态的模型,它将视频片段与文本在同一个共享嵌入空间中对齐,从而仅用一个预训练主干即可完成零样本视频分类和视频-文本检索。本文以 pe_video.md 为骨架,结合 modeling_pe_video.py 等源码,从模型设计、配置体系、预处理细节到推理实践,完整还原该模型在 🤗 Transformers 中的接入方式与底层原理。读完本文,你将能独立加载facebook/pe-av-large权重,对任意视频做带文本标签的零样本打分,并理解其帧采样、padding mask、RoPE 时间轴等设计取舍。
模型背景:Meta Perception Encoder 家族的视频分支
根据仓库文档 pe_video.md 的记录:PE Video 模型于 2025-04-17 发布,并在 2025-12-16 被合入 Hugging Face Transformers。它隶属于 Meta 的 Perception Encoder(PE)模型家族(论文编号 arXiv 2504.13181),是该家族的视频(video)分支——同族还包括音频(audio)等分支,并共享同一套对比学习范式。
PE Video 的核心思想非常简洁:用对比学习把"视频片段"与"文本描述"对齐到同一个共享嵌入空间。训练完成后,这一个预训练主干就能直接做两件事:
- 零样本视频分类:给定若干文本候选类别,计算视频与每个类别的相似度并打分;
- 视频-文本检索:把视频嵌入与文本嵌入在共享空间内做相似度排序,即可互相检索。
模型仓库中与本文强相关的入口还包括快速上手用的facebook/pe-av-large检查点(它同时涵盖感知编码器的音视频能力,这也是为什么仓库文档将官方检查点集合统称为 perception-encoder-audio-visual)。需要注意:facebook/pe-av-large是共享的音频-视觉检查点,PE Video 模块加载它后会使用其中的视频塔。
Quickstart:一行行跑通零样本视频分类
文档 pe_video.md 给出了一个可直接运行的完整示例。我们把它拆开并结合源码细节逐段讲解。
import torch from transformers import AutoProcessor, PeVideoModel from transformers.video_utils import load_video processor = AutoProcessor.from_pretrained("facebook/pe-av-large") model = PeVideoModel.from_pretrained( "facebook/pe-av-large", device_map="auto", ) video, _ = load_video("https://huggingface.co/datasets/hf-internal-testing/fixtures_videos/resolve/main/tennis.mp4") labels = ["a person playing tennis", "a person cooking", "a cat sleeping"] video_inputs = processor.video_processor(video, num_frames=16, return_tensors="pt").to(model.device) text_inputs = processor.tokenizer(labels, padding=True, return_tensors="pt").to(model.device) inputs = {**video_inputs, **text_inputs} with torch.no_grad(): outputs = model(**inputs) probs = outputs.logits_video_text.sigmoid() print({label: p.item() for label, p in zip(labels, probs[0])})代码里每一行都值得展开说明:
预处理分两步:
PeVideoProcessor(见 processing_pe_video.py)是一个轻量ProcessorMixin,同时暴露video_processor(视频塔)与tokenizer(文本塔)两个属性,二者分别处理视频帧与文本标签,最后用解包语法{**video_inputs, **text_inputs}拼成模型输入。video_processor负责视频塔输入:视频需要先抽帧、再归一化缩放,产出键为pixel_values_videos的张量。传给num_frames=16表示均匀抽取 16 帧(采样细节见下文"时间轴处理"小节);return_tensors="pt"决定是否做变长 padding 并返回padding_mask_videos。tokenizer负责文本塔输入:把候选标签文本批量 tokenize,得到input_ids与attention_mask。logits_video_text是行语义的关键:如 modeling_pe_video.py 所示,logits_video_text = video_embeds @ text_video_embeds.T,即视频嵌入与各条文本嵌入的内积,形状为(视频条数, 文本条数)。随后逐元素做sigmoid,即可把 logits 转成 0~1 的多标签式概率,因此示例里对每个标签输出一个独立概率。若只放一条视频、若干候选文本,probs[0]恰好就是这条视频在每个候选类别上的置信度——这就是零样本分类的核心。
若想控制 loss 或拿原始嵌入,
PeVideoOutput(modeling_pe_video.py)会返回logits_video_text、text_video_embeds、video_embeds、两塔各自的outputs以及可选的loss字段。
三条关键使用要点(Usage tips)的源码级解读
文档在 "Usage tips and notes" 里强调了三个最容易踩坑的点,我们逐条追到源码。
1. 变长视频请用padding_mask_videos,而不是attention_mask
视频塔内部的 self-attention 需要"哪些帧是真实帧"的指示,这个角色由padding_mask_videos承担;文本塔的attention_mask不参与视频帧的遮蔽。
关键行为藏在 video_processing_pe_video.py 的PeVideoVideoProcessor._preprocess中:
def _preprocess(self, videos, **kwargs): # Always set `return_tensors` to `None` since it won't pad variable length videos # We'll handle this after we call the parent's method return_tensors = kwargs.pop("return_tensors", None) result = super()._preprocess(videos, **kwargs) pixels = result.pixel_values_videos data = {"pixel_values_videos": pixels} if return_tensors: lengths = torch.tensor([video.size(0) for video in pixels]) pixels = torch.nn.utils.rnn.pad_sequence(pixels, batch_first=True, padding_value=0.0) data["pixel_values_videos"] = pixels if lengths.unique().size(0) > 1: mask = torch.arange(lengths.max())[None] < lengths[:, None] data["padding_mask_videos"] = mask return BatchFeature(data=data, tensor_type=return_tensors)- 处理变长视频时,父类逻辑不负责 padding,因此子类先强制把
return_tensors弹出、以列表形式拿到各条 clip 的帧张量; - 仅当调用方显式传入
return_tensors(如"pt")时,才用pad_sequence把变长帧序列按最长帧数对齐为(batch_size, num_frames, C, H, W),填充值 0.0; padding_mask_videos只会在批内长度不一致时返回:实现用torch.arange(lengths.max())[None] < lengths[:, None]生成布尔 mask,1表示真实帧、0表示 padding 帧(PeVideoEncoder.forward 的 docstring 同样明确了这一语义);- 反之,若不传
return_tensors,你得到的是一组"每条视频一个张量"的列表,且没有mask。
换句话说:处理变长视频时必须同时满足"传return_tensors+ 拿padding_mask_videos"两个条件,否则要么无法 padding、要么丢了 mask。测试 test_modeling_pe_video.py 中也刻意构造了valid_lengths在[1, num_frames]区间内的随机合法长度来覆盖这一路径。
2.num_frames决定均匀采样,缺省则退回基于 fps 的采样
视频塔把时间轴当做一个真实存在的维度,因此"到底取哪些帧"非常重要。video_processing_pe_video.py 中的PeVideoVideoProcessor.sample_frames实现为:
def sample_frames(self, metadata, num_frames=None, fps=None, **kwargs): if num_frames: total_frames = metadata.total_num_frames num_frames = num_frames if num_frames is not None else self.num_frames frame_idxs = [int(i * (total_frames - 1) / (num_frames - 1)) for i in range(num_frames)] return torch.tensor(frame_idxs) else: return super().sample_frames(metadata, num_frames, fps, **kwargs)- 传入
num_frames时,采用固定长度均匀采样:在闭区间[0, total_frames - 1]上等间距取帧,frame_idxs首尾一定覆盖视频的第一帧和最后一帧; - 不传
num_frames时,if num_frames:为假,直接回退到基类BaseVideoProcessor.sample_frames的基于 fps 的采样逻辑。
文档特别提醒:检查点在训练时通常针对特定的帧数(quickstart 用的num_frames=16就是常见取值),因此推理时应尽量匹配检查点训练时使用的帧数,不要随意切换采样策略,否则分布偏移可能显著影响打分质量。底层视频解码则统一由 video_utils.py 的load_video完成——支持本地路径与 URL,默认后端为pyav,也可显式选择decord / opencv / torchvision / torchcodec,并规定num_frames、fps、sample_indices_fn三者互斥。
3.main_input_name的差异会影响通用工具路由
这一点关系到 Transformers 生态里的通用工具代码。两个类的main_input_name并不相同:
- 视频编码器
PeVideoEncoder的main_input_name = "pixel_values_videos"(modeling_pe_video.py 的PeVideoPreTrainedModel与 PeVideoEncoder 均如此); - 而完整模型
PeVideoModel的main_input_name = "input_ids"(modeling_pe_video.py)。
原因很直观:PeVideoModel是"文本塔 + 视频塔"的双塔结构,通用 API(例如某些 pipeline、序列化或参数路由代码)会通过main_input_name判断"主输入是文本还是视觉特征",而对双塔模型而言,文本侧的input_ids才是惯例上的主入口。如果你写代码时依赖model.main_input_name来决定把张量放到哪个字段,请务必分清对象是PeVideoEncoder还是PeVideoModel。
架构原理:双塔 + 共享嵌入空间
从 PeVideoConfig 与 PeVideoModel 可以看出,PE Video 是典型的双塔对比结构:
┌──────────── 文本塔 ────────────┐ input_ids ───►│ ModernBERT (AutoModel) │──► text_video_head ──► text_video_embeds └────────────────────────────────┘ ▼ logits_video_text = video_embeds @ text_video_embeds.T ▲ ┌──────────── 视频塔 ────────────┐ pixel_values ──►│ Timm ViT → patch embedder │ videos │ → 6× Transformer → pooler │──► video_head ──► video_embeds └────────────────────────────────┘4.1 视频塔(PeVideoEncoder)
PeVideoEncoder(modeling_pe_video.py)的 forward 依次经过四个阶段:
- 帧嵌入(
PeVideoEncoderEmbedder):把(batch, num_frames, C, H, W)展平成(batch*num_frames, C, H, W),喂给AutoModelForImageClassification.from_config(config.vision_config)所构造的视觉主干——默认是 timm wrapper 下的vit_pe_core_large_patch14_336(ViT-Large、patch 14、输入 336×336,见 configuration_pe_video.py)。每帧被压成一个 logits 向量,随后F.normalize、两层线性映射(proj+data_proj)得到逐帧 token 序列,即"patch embedder 的输入 embeds"; - Patch Embedder(
PeVideoEncoderPatchEmbedder):在序列最前面拼接一个可学习的class token,然后送入 1D ResNet 块(含 PeVideoMaskedGroupNorm + SiLU + kernel=3 的 Conv1d)。GroupNorm 被改造成只对真实帧做统计(padding_mask参与 mean/var 计算并对输出做* padding_mask),从而保证 padding 帧不会污染归一化统计量; - RoPE + 6 层 Transformer:class token 与帧序列一起进入
PeVideoEncoderLayer(RMSNorm、GQA 风格多头注意力 + q/k 上的 RMSNorm、SwiGLU MLP)。attention 是双向的(self.is_causal = False),并通过create_bidirectional_mask把帧 mask 转换成注意力 mask; - 输出投影:最后经 RMSNorm 与无 bias 线性层后,
PeVideoEncoder.forward返回BaseModelOutputWithPooling——last_hidden_state = hidden_states[:, 1:](去掉 class token 的逐帧输出),pooler_output = hidden_states[:, 0](class token 即视频级表征)。
值得注意的是,RoPE 位置编码(PeVideoEncoderRotaryEmbedding)和 patch embedder 都把时间轴当作第一等公民维度来编码:位置 id 沿帧序列生成,rope_theta=20000按head_dim计算逆频率。正因为时间信息被 RoPE 烘焙进序列内部,模型才能直接编码变长片段(配合 mask),而不必"逐帧独立 tile"后手工融合。
4.2 文本塔与对比头
- 文本塔通过
AutoModel.from_config(config.text_config)构造,默认是ModernBERT主干(hidden_size=1024, intermediate_size=2624, num_hidden_layers=22, num_attention_heads=16,见 configuration_pe_video.py); PeVideoModel.forward(modeling_pe_video.py)里文本侧取text_outputs.hidden_states[-1][:, 0](CLS 位置)作为文本序列表征;- 视频侧直接取
video_outputs.pooler_output; - 两侧分别过
PeVideoContrastiveHead(LayerNorm + 无 bias 线性投影,modeling_pe_video.py),投影到同一hidden_size空间; - 最终 logits 还要经过可学习的
text_video_logit_scale与text_video_logit_bias缩放平移。
4.3 对比损失
当return_loss=True时(modeling_pe_video.py),代码用单位矩阵torch.eye(batch_size)作为标签,对labels * logits_video_text计算-logsigmoid(...).sum() / batch_size——这是一个在批内自建正负样本对(对角线为正对)的对比式 sigmoid loss,即视频塔与文本塔的对齐信号来源。推理时你无需 loss,只需对 logits 做sigmoid()得到各标签的概率。
配置参数全景
PE Video 提供两级配置:完整模型PeVideoConfig与其子配置PeVideoEncoderConfig。二者都标记了base_config_key = "audio_video_config"(见 configuration_pe_video.py),说明在感知编码器家族里音视频配置结构是同构的、可互换的,这也呼应了"音视频共享检查点"的生态设计。
PeVideoConfig(完整双塔模型,model_type = "pe_video"):
| 字段 | 默认值 | 说明 |
|---|---|---|
text_config | None(回退到默认 ModernBERT 参数) | 文本塔配置;可传 dict 或PreTrainedConfig,dict 会与默认 ModernBERT 参数合并 |
video_config | None | 视频塔配置;可传 dict 或PreTrainedConfig,最终实例化为PeVideoEncoderConfig |
model_type | "pe_video" | 注册的模型类型标识 |
PeVideoEncoderConfig(视频编码器,model_type = "pe_video_encoder"):
| 字段 | 默认值 | 说明 |
|---|---|---|
vision_config | timm_wrapper:vit_pe_core_large_patch14_336,do_pooling=True,num_classes=1024,global_pool="map" | 逐帧视觉主干(TimmWrapper),负责把每帧压成向量 |
hidden_size | 1792 | Transformer 隐藏维度 |
intermediate_size | 4800 | MLP 中间维度 |
num_hidden_layers | 6 | Transformer 层数 |
num_attention_heads | 14 | 注意力头数 |
num_key_value_heads | None(自动等于num_attention_heads) | GQA 的 KV 头数,为None时退化为 MHA |
head_dim | 128 | 每个注意力头的维度,RoPE 按它计算逆频率 |
hidden_act | "silu" | MLP 激活函数(配合 SwiGLU 门控) |
max_position_embeddings | 10000 | RoPE 缓存的最大序列长度 |
initializer_range | 0.02 | 初始化标准差 |
rms_norm_eps | 1e-5 | RMSNorm 的 eps |
rope_parameters | {"rope_theta": 20000} | RoPE 超参(theta),可切换高级 rope_type |
attention_bias | False | 注意力投影是否带 bias |
attention_dropout | 0.0 | 注意力 dropout,训练时生效 |
配置类本身是带@strict的 dataclass(来自 huggingface_hub 的严格校验),并在__post_init__中完成默认值推导与嵌套配置实例化——即把num_key_value_heads=None填成注意力头数、把rope_parameters填成{"rope_theta": 20000}、把vision_configdict 交给CONFIG_MAPPING["timm_wrapper"]渲染。
推理输入/输出约定与测试佐证
输入约定(PeVideoModel.forward,modeling_pe_video.py):
input_ids:文本塔的 token id,形状(batch_size_text, seq_len);attention_mask:文本塔注意力 mask;pixel_values_videos:视频塔输入,(batch_size, num_frames, C, H, W);padding_mask_videos:(batch_size, num_frames),1表示真实帧、0表示 padding 帧;return_loss:置True时计算对比 loss。
输出约定:PeVideoOutput中含logits_video_text(相似度 logits)、text_video_embeds、video_embeds、两塔各自的隐藏输出text_outputs/video_outputs,以及可选的loss。
单元测试 test_modeling_pe_video.py 印证了以上约定:测试构造pixel_values_videos与随机合法长度的padding_mask_videos,直接调用PeVideoEncoder(pixel_values_videos, padding_mask_videos=...)与PeVideoModel(input_ids, pixel_values_videos, attention_mask, padding_mask_videos),并把pixel_values_videos、padding_mask_videos列入模型的额外输入键集合。
常见陷阱与使用建议
- 变长视频必须"成对"使用:
processor.video_processor(..., return_tensors=...)与padding_mask_videos要么同时生效、要么同时缺席。只传张量而忽略 mask,会在批量长度不一时让 padding 帧参与注意力(GroupNorm 虽然屏蔽了 padding,但 Transformer 层需要 mask 配合create_bidirectional_mask才能真正屏蔽注意力)。 - 帧数对齐检查点:固定长度均匀采样是 PE Video 的默认路径;
facebook/pe-av-large一类的检查点通常在固定帧数下训练,推理时最好使用与训练一致的num_frames。想要 fps 采样需显式不传num_frames,此时会退回基类实现。 - 区分两个模型的
main_input_name:视频塔是pixel_values_videos、完整模型是input_ids。任何"读取main_input_name来猜测输入类型"的通用代码,都必须先弄清拿到的是哪一级对象。 - 文本候选用批量 tokenize:quickstart 里把多个标签文本一次 tokenize(
padding=True),logits_video_text的第 0 维对应视频条数、第 1 维对应标签文本数,用sigmoid而非softmax(标签间不是互斥关系,可做多标签式打分)。 - 解码后端可选:
load_video默认pyav,加载长视频可考虑显式传backend="torchcodec"提升解码效率(需额外安装对应依赖,见 video_utils.py 的可用后端枚举)。
PE Video 的接入完整遵循 Transformers 的标准组件划分——PreTrainedConfig(配置)、PreTrainedModel(建模)、ProcessorMixin(预处理)三者解耦,同时通过"帧级视觉主干 + 时间序列 Transformer + RoPE"的组合,把时间轴变成了真正参与注意力计算的维度。理解这一结构后,无论做零样本分类、视频-文本检索,还是后续微调,你都能准确地组织输入并预判模型行为。
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考