news 2026/9/8 20:25:16

Transformers 中的 PeVideo:面向零样本视频分类与视频-文本检索的对比学习视频编码器实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Transformers 中的 PeVideo:面向零样本视频分类与视频-文本检索的对比学习视频编码器实战指南

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])})

代码里每一行都值得展开说明:

  1. 预处理分两步PeVideoProcessor(见 processing_pe_video.py)是一个轻量ProcessorMixin,同时暴露video_processor(视频塔)与tokenizer(文本塔)两个属性,二者分别处理视频帧与文本标签,最后用解包语法{**video_inputs, **text_inputs}拼成模型输入。

  2. video_processor负责视频塔输入:视频需要先抽帧、再归一化缩放,产出键为pixel_values_videos的张量。传给num_frames=16表示均匀抽取 16 帧(采样细节见下文"时间轴处理"小节);return_tensors="pt"决定是否做变长 padding 并返回padding_mask_videos

  3. tokenizer负责文本塔输入:把候选标签文本批量 tokenize,得到input_idsattention_mask

  4. 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_texttext_video_embedsvideo_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_framesfpssample_indices_fn三者互斥。

3.main_input_name的差异会影响通用工具路由

这一点关系到 Transformers 生态里的通用工具代码。两个类的main_input_name并不相同:

  • 视频编码器PeVideoEncodermain_input_name = "pixel_values_videos"(modeling_pe_video.py 的PeVideoPreTrainedModel与 PeVideoEncoder 均如此);
  • 而完整模型PeVideoModelmain_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 依次经过四个阶段:

  1. 帧嵌入(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";
  2. Patch Embedder(PeVideoEncoderPatchEmbedder:在序列最前面拼接一个可学习的class token,然后送入 1D ResNet 块(含 PeVideoMaskedGroupNorm + SiLU + kernel=3 的 Conv1d)。GroupNorm 被改造成只对真实帧做统计padding_mask参与 mean/var 计算并对输出做* padding_mask),从而保证 padding 帧不会污染归一化统计量;
  3. RoPE + 6 层 Transformer:class token 与帧序列一起进入PeVideoEncoderLayer(RMSNorm、GQA 风格多头注意力 + q/k 上的 RMSNorm、SwiGLU MLP)。attention 是双向的(self.is_causal = False),并通过create_bidirectional_mask把帧 mask 转换成注意力 mask;
  4. 输出投影:最后经 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=20000head_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_scaletext_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_configNone(回退到默认 ModernBERT 参数)文本塔配置;可传 dict 或PreTrainedConfig,dict 会与默认 ModernBERT 参数合并
video_configNone视频塔配置;可传 dict 或PreTrainedConfig,最终实例化为PeVideoEncoderConfig
model_type"pe_video"注册的模型类型标识

PeVideoEncoderConfig(视频编码器,model_type = "pe_video_encoder"):

字段默认值说明
vision_configtimm_wrapper:vit_pe_core_large_patch14_336do_pooling=Truenum_classes=1024global_pool="map"逐帧视觉主干(TimmWrapper),负责把每帧压成向量
hidden_size1792Transformer 隐藏维度
intermediate_size4800MLP 中间维度
num_hidden_layers6Transformer 层数
num_attention_heads14注意力头数
num_key_value_headsNone(自动等于num_attention_headsGQA 的 KV 头数,为None时退化为 MHA
head_dim128每个注意力头的维度,RoPE 按它计算逆频率
hidden_act"silu"MLP 激活函数(配合 SwiGLU 门控)
max_position_embeddings10000RoPE 缓存的最大序列长度
initializer_range0.02初始化标准差
rms_norm_eps1e-5RMSNorm 的 eps
rope_parameters{"rope_theta": 20000}RoPE 超参(theta),可切换高级 rope_type
attention_biasFalse注意力投影是否带 bias
attention_dropout0.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_embedsvideo_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_videospadding_mask_videos列入模型的额外输入键集合。

常见陷阱与使用建议

  1. 变长视频必须"成对"使用processor.video_processor(..., return_tensors=...)padding_mask_videos要么同时生效、要么同时缺席。只传张量而忽略 mask,会在批量长度不一时让 padding 帧参与注意力(GroupNorm 虽然屏蔽了 padding,但 Transformer 层需要 mask 配合create_bidirectional_mask才能真正屏蔽注意力)。
  2. 帧数对齐检查点:固定长度均匀采样是 PE Video 的默认路径;facebook/pe-av-large一类的检查点通常在固定帧数下训练,推理时最好使用与训练一致的num_frames。想要 fps 采样需显式不传num_frames,此时会退回基类实现。
  3. 区分两个模型的main_input_name:视频塔是pixel_values_videos、完整模型是input_ids。任何"读取main_input_name来猜测输入类型"的通用代码,都必须先弄清拿到的是哪一级对象。
  4. 文本候选用批量 tokenize:quickstart 里把多个标签文本一次 tokenize(padding=True),logits_video_text的第 0 维对应视频条数、第 1 维对应标签文本数,用sigmoid而非softmax(标签间不是互斥关系,可做多标签式打分)。
  5. 解码后端可选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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 20:24:00

『Hello アルゴリズム』スタック・キュー章 総まとめ:LIFO/FIFO の核心 5 要点と配列・連結リスト実装の比較、章末 QA をソースコードで徹底解説

『Hello アルゴリズム』スタック・キュー章 総まとめ&#xff1a;LIFO/FIFO の核心 5 要点と配列・連結リスト実装の比較、章末 Q&A をソースコードで徹底解説 【免费下载链接】hello-algo 《Hello 算法》&#xff1a;动画图解、一键运行的数据结构与算法教程。支持简中、繁…

作者头像 李华
网站建设 2026/9/8 20:20:33

极空间NAS部署道理鱼:音乐/MV/有声书全栈媒体库完整指南

一直在折腾家里的极空间NAS&#xff0c;从最开始的纯文件存储&#xff0c;到后来跑Jellyfin看剧、部署各种自动化工具&#xff0c;慢慢感觉这台机器的功能越挖越深。前两天为了给车上的音乐库和跑步时候听的有声书找一个统一入口&#xff0c;盯上了一个叫『道理鱼』&#xff08…

作者头像 李华
网站建设 2026/9/8 20:19:51

FPGA 100G光口光模块测试实战:从GT配置到误码分析

1. 项目概述与测试目标拆解做FPGA开发这些年&#xff0c;凡是和高速接口沾边的项目&#xff0c;最终基本都会绕到光口上来。尤其是100G这个速率档位&#xff0c;从数据中心到仪器仪表&#xff0c;从通信设备到视频传输&#xff0c;几乎成了标配。我这段时间正好在调试一块带100…

作者头像 李华