MoViNets 实战指南:基于 TensorFlow models 仓库实现移动端高效视频动作识别
【免费下载链接】modelsModels and examples built with TensorFlow项目地址: https://gitcode.com/GitHub_Trending/mode/models
本文以 TensorFlow models 仓库中的 MoViNet 官方实现(official/projects/movinet/)为主体,完整讲解 MoViNets 这一"可在移动端运行的视频分类模型族"的设计动机、Base 与 Streaming 两类模型的区别、Kinetics 400/600 预训练权重与 TF Lite 延迟数据、从 Python 推理到 TF Lite 部署再到训练评测的完整流程。读完本文,你可以独立构建 MoViNet 骨干网络、以逐帧流式方式对视频进行动作分类、将模型导出为 TF Lite 并在 CPU 上推理,也能复现其 Kinetics 600 训练配置。
MoViNets(Mobile Video Networks)是论文MoViNets: Mobile Video Networks for Efficient Video Recognition(arXiv:2103.11511)的官方 TensorFlow 实现,定位为"既准确又能在移动设备上跑"的视频分类模型族,覆盖从最小的 MoViNet-T0/A0 到最大的 MoViNet-A5/A6。仓库中该模块的入口文档为 official/projects/movinet/README.md,核心实现在 official/projects/movinet/modeling/movinet.py 与 official/projects/movinet/modeling/movinet_model.py。
设计动机:弥合 2D 高效 CNN 与 3D 高精度 CNN 之间的鸿沟
视频动作识别领域长期存在一个矛盾:2D MobileNet 类 CNN 速度快、能实时处理流式视频,但对时序建模弱,预测噪声大;3D CNN 准确度高,但显存与算力开销大,无法用于流式视频。MoViNets 用三种手段弥合这一差距(README "Description" 一节):
- 全模型族的效率-精度权衡:MoViNet-A0 到 A6 共 7 个规格,从 2.7 GFLOPs 到 280 GFLOPs 覆盖不同部署预算;
- 流式(Streaming)模型:采用因果(causal)卷积加流缓冲区(stream buffer),显著降低内存占用,可逐帧处理无限长视频;
- 时序集成(Temporal ensembles):例如 MoViNet-A6 可由 MoViNet-A4 与 A5 组合而成,进一步提升精度。
README 给出的代表性数据:在 Kinetics 600 上,MoViNet-A6 达到 84.8% top-1 精度,优于 ViViT(83.0%)与 VATT(83.6%)等视觉 Transformer 方案且 FLOPs 少约 10 倍;流式 MoViNet-A0 达到 72% 精度,比 MobileNetV3-large(68%)FLOPs 少约 3 倍。此外,MoViNets 逐帧输出高质量预测,避免了传统多片段(multi-clip)评测方式带来的重复计算与时序范围受限问题。
模型家族、预训练权重与 TF Lite 延迟
Kinetics 600 基础模型(Base)
Base 模型实现标准 3D 卷积、不含流缓冲区。由于 CPU/移动端对tf.nn.conv3d支持有限,README 明确提示 Base 模型不推荐用于 CPU 或移动端快速推理,移动端请使用下面的流式模型。
| 模型 | Top-1 | Top-5 | 输入形状(帧 x 高 x 宽) | GFLOPs |
|---|---|---|---|---|
| MoViNet-A0-Base | 72.28 | 90.92 | 50 x 172 x 172 | 2.7 |
| MoViNet-A1-Base | 76.69 | 93.40 | 50 x 172 x 172 | 6.0 |
| MoViNet-A2-Base | 78.62 | 94.17 | 50 x 224 x 224 | 10 |
| MoViNet-A3-Base | 81.79 | 95.67 | 120 x 256 x 256 | 57 |
| MoViNet-A4-Base | 83.48 | 96.16 | 80 x 290 x 290 | 110 |
| MoViNet-A5-Base | 84.27 | 96.39 | 120 x 320 x 320 | 280 |
表中 GFLOPs 为 Kinetics 600 上单视频的算力开销;所有模型均以与训练相同分辨率的单片段评测。MoViNet-A6 可构造为 A4 与 A5 的集成。
Kinetics 600 流式模型(Streaming)
流式模型使用因果 (2+1)D 卷积加流缓冲区:以 (2+1)D 卷积替代 3D 卷积,从而调用经过优化的tf.nn.conv2d算子,在 CPU 上获得快速推理。流式模型既可以逐帧运行,也可以像 Base 模型一样整段视频一次运行。
| 模型 | Top-1 | Top-5 | 输入形状* | GFLOPs |
|---|---|---|---|---|
| MoViNet-A0-Stream | 72.05 | 90.63 | 50 x 172 x 172 | 2.7 |
| MoViNet-A1-Stream | 76.45 | 93.25 | 50 x 172 x 172 | 6.0 |
| MoViNet-A2-Stream | 78.40 | 94.05 | 50 x 224 x 224 | 10 |
| MoViNet-A3-Stream | 80.09 | 94.84 | 120 x 256 x 256 | 57 |
| MoViNet-A4-Stream | 81.49 | 95.66 | 80 x 290 x 290 | 110 |
| MoViNet-A5-Stream | 82.37 | 95.79 | 120 x 320 x 320 | 280 |
*流式模式下,"帧数"对应 10 秒片段累积的总时长。注意两点实现细节(README 明确指出):
- 位置编码(Positional Encoding)只用于 A3/A4/A5:这三个大模型的 squeeze-excitation 块带有位置编码,去掉后精度明显下降;A0/A1/A2 则不带,精度不受影响。这与源码一致——在 official/projects/movinet/modeling/movinet.py 的
_build_network中,use_positional_encoding只有在self._causal为真时才实际生效(第 502-503 行)。 - 当前流式 checkpoint 已更新为略有差异的新架构;需要旧版 checkpoint 时在 URL 中
.tar.gz前插入_legacy(如movinet_a0_stream_legacy.tar.gz)。
TF Lite 流式模型与单帧延迟
仓库提供转换好的 TF Lite 模型(float32 单帧延迟,单 CPU 核心,实测于 Pixel 4/Android 11 与 x86 Intel Xeon W-2135):
| 模型 | 输入形状 | Pixel 4 延迟 | x86 延迟 | 文件大小 |
|---|---|---|---|---|
| MoViNet-A0-Stream | 1 x 1 x 172 x 172 | 22 ms | 16 ms | 13 MB |
| MoViNet-A1-Stream | 1 x 1 x 172 x 172 | 42 ms | 33 ms | 45 MB |
| MoViNet-A2-Stream | 1 x 1 x 224 x 224 | 200 ms | 66 ms | 53 MB |
| MoViNet-A3-Stream | 1 x 1 x 256 x 256 | - | 120 ms | 73 MB |
| MoViNet-A4-Stream | 1 x 1 x 290 x 290 | - | 300 ms | 101 MB |
| MoViNet-A5-Stream | 1 x 1 x 320 x 320 | - | 450 ms | 153 MB |
作为参照,MoViNet-A0-Stream 的延迟与 MobileNetV3-Large(224 输入)相当,但 Kinetics 600 上 top-1 精度高约 5 个百分点。此外还有量化版 TF Lite 模型可经 TF Hub 获取(2022-03-14 更新)。
Kinetics 400 权重
仓库同时提供 Kinetics 400 的 Base 模型 checkpoint:A0-Base 69.40%、A1-Base 74.57%、A2-Base 75.91%、A3-Base 79.34%、A4-Base 80.64%、A5-Base 81.39%(Top-1,输入形状与 GFLOPs 与 Kinetics 600 对应模型一致)。加载时只需把分类头设置为num_classes=400。
架构细节:从 BLOCK_SPECS 到流状态
骨干结构的声明式定义
official/projects/movinet/modeling/movinet.py 用三个 dataclass 声明每个模型的结构:StemSpec(输入块:filters、kernel_size、strides)、MovinetBlockSpec(每个 block 内的若干层:base_filters、expand_filters、kernel_sizes、strides)与HeadSpec(project_filters、head_filters)。文件顶部定义了两个命名常量便于书写规格:K13/K15/K33/K53表示 (1,3,3)、(1,5,5)、(3,3,3)、(5,3,3) 的 3D 卷积核,S11/S12/S22/S21表示对应的步幅。以 MoViNet-A0 为例(第 81-109 行):
'a0': ( StemSpec(filters=8, kernel_size=K13, strides=S12), MovinetBlockSpec(base_filters=8, expand_filters=(24,), kernel_sizes=(K15,), strides=(S12,)), MovinetBlockSpec(base_filters=32, expand_filters=(80, 80, 80), kernel_sizes=(K33, K33, K33), strides=(S12, S11, S11)), # ... 共 5 个 MovinetBlockSpec ... HeadSpec(project_filters=480, head_filters=2048), )BLOCK_SPECS字典中定义了a0~a5以及更小的t0(比 A0 更小更快的规格)。从源码结构看,模型规模随model_id增大而主要体现在:空间卷积核更宽(K53 出现更多)、块数更多、expand_filters 更大;所有模型共享head_filters=2048(t0 为 1024)的分类头宽度。
卷积类型 conv_type 的三种取值
Movinet.__init__对conv_type的校验与注释(movinet.py 第 335-344 行)给出了三种取值的精确定义:
'3d':默认 3D 卷积;'2plus1d':(2+1)D 卷积,底层用Conv2D + 2D 重塑实现——例如一个 5x3x3 卷积核拆为先 3x3、再 5x1 的卷积,这正是流式模型在 CPU 上快的重要原因;'3d_2plus1d':(2+1)D 卷积但底层用 Conv3D(如 5x3x3 拆为 1x3x3 加 5x1x1),训练吞吐更好,但不适合直接导出给 TF Lite,需先用工具转换(见下文 TF Lite 一节)。
se_type(squeeze-excitation 的全局池化方式)取值'3d'/'2d'/'2plus3d'/'none',其中'2plus3d'是 2D 与 3D 全局平均池化结果的拼接——流式 checkpoint 正是用se_type='2plus3d'。
流状态(states)是流式推理的核心
流式模型把"记忆"显式化为一个状态字典。从 movinet.py 的_get_initial_state_shapes(第 539-634 行)可以看到状态名的构造规则:
- 每个含时间卷积核的层拥有
state_block{b}_layer{l}_stream_buffer,形状中时间维为kernel_size[0] - 1(即缓存卷积核宽度减一的过去帧特征); - 每个使用 3D squeeze-excitation 的层拥有
state_block{b}_layer{l}_pool_buffer(形状 [B,1,1,1,C],累积求和缓冲)与state_block{b}_layer{l}_pool_frame_count(int32 帧计数,用于累积式全局平均池化); - 开启位置编码时额外有
*_pos_enc_frame_count;Head 处有state_head_pool_buffer与state_head_pool_frame_count。
对应地,init_states(input_shape)会返回所有状态置零的字典(frame_count类状态用 int32,其余用模型 dtype);initial_state_specs则返回同名InputSpec字典,供 Keras 建图时作为额外输入。分类模型 MovinetClassifier 进一步封装了这套机制:当骨干use_external_states=True时,__init__会为每个状态创建tf_keras.Input,使模型输入变成{**states, 'image'}的字典,输出为(logits, states)二元组;_build_backbone还显式校验了输入/输出状态集合与形状的一致性(第 119-144 行),防止状态名不匹配导致的静默错误。
层次实现位于 official/projects/movinet/modeling/movinet_layers.py,其中StreamBuffer、StreamConvBlock、StreamSqueezeExcitation、MobileBottleneck、SkipBlock、MovinetBlock、Stem、Head、ClassifierHead等类分别对应上述结构组件。
推理示例:Base 与 Streaming 两种用法
以下两段代码完整继承自 README "Prediction Examples",可直接复制运行(需先pip install -rofficial/projects/movinet/requirements.txt)。
Base 模型一次性推理
import tensorflow as tf from official.projects.movinet.modeling import movinet from official.projects.movinet.modeling import movinet_model # Create backbone and model. backbone = movinet.Movinet( model_id='a0', causal=False, use_external_states=False, ) model = movinet_model.MovinetClassifier( backbone, num_classes=600, output_states=False) # Create your example input here. # Refer to the paper for recommended input shapes. inputs = tf.ones([1, 8, 172, 172, 3]) # [Optional] Build the model and load a pretrained checkpoint model.build(inputs.shape) checkpoint_dir = '/path/to/checkpoint' checkpoint_path = tf.train.latest_checkpoint(checkpoint_dir) checkpoint = tf.train.Checkpoint(model=model) status = checkpoint.restore(checkpoint_path) status.assert_existing_objects_matched() # Run the model prediction. output = model(inputs) prediction = tf.argmax(output, -1)要点:Base 模型causal=False、use_external_states=False,调用方式就是普通的 Keras 前向传播,输入为[batch, num_frames, H, W, 3]的整段视频。
Streaming 模型逐帧推理
import tensorflow as tf from official.projects.movinet.modeling import movinet from official.projects.movinet.modeling import movinet_model model_id = 'a0' use_positional_encoding = model_id in {'a3', 'a4', 'a5'} # Create backbone and model. backbone = movinet.Movinet( model_id=model_id, causal=True, conv_type='2plus1d', se_type='2plus3d', activation='hard_swish', gating_activation='hard_sigmoid', use_positional_encoding=use_positional_encoding, use_external_states=True, ) model = movinet_model.MovinetClassifier( backbone, num_classes=600, output_states=True) # Create your example input here. inputs = tf.ones([1, 8, 172, 172, 3]) # [Optional] Build the model and load a pretrained checkpoint. model.build(inputs.shape) checkpoint_dir = '/path/to/checkpoint' checkpoint_path = tf.train.latest_checkpoint(checkpoint_dir) checkpoint = tf.train.Checkpoint(model=model) status = checkpoint.restore(checkpoint_path) status.assert_existing_objects_matched() # Split the video into individual frames. # Note: we can also split into larger clips as well (e.g., 8-frame clips). # Running on larger clips will slightly reduce latency overhead, but # will consume more memory. frames = tf.split(inputs, inputs.shape[1], axis=1) # Initialize the dict of states. All state tensors are initially zeros. init_states = model.init_states(tf.shape(inputs)) # Run the model prediction by looping over each frame. states = init_states predictions = [] for frame in frames: output, states = model({**states, 'image': frame}) predictions.append(output) # The video classification will simply be the last output of the model. final_prediction = tf.argmax(predictions[-1], -1) # Alternatively, we can run the network on the entire input video. # The output should be effectively the same # (but it may differ a small amount due to floating point errors). non_streaming_output, _ = model({**init_states, 'image': inputs}) non_streaming_prediction = tf.argmax(non_streaming_output, -1)这里有几个值得对照源码理解的细节:
- 流式模型必须同时满足
causal=True与use_external_states=True。movinet.py 第 398-399 行显式抛出异常:非 causal 模式下使用外部状态会被拒绝; init_states(tf.shape(inputs))返回的状态全部为 0 张量,与_get_initial_state_shapes推导的形状一一对应;- 逐帧循环时,
model({**states, 'image': frame})的返回值是(logits, states)——下一轮的states直接覆盖上一轮输出,这正是"状态字典"设计的意义:缓冲区、累积池化和帧计数都随帧推进自动更新; - 帧可以换成更大的片段(如 8 帧),能略微降低调用开销但会增加内存;整段视频一次跑通与逐帧循环结果在浮点误差范围内应一致,README 也把这一点作为可验证的预期行为;
- 视频级标签取最后一帧的输出(
predictions[-1]),因为因果模型逐帧输出的是截至当前的预测。
仓库还附带两个可直接运行的示例:动作识别演示 GIF(official/projects/movinet/files/jumpingjack.gif)以及 Kinetics 600 类别表 official/projects/movinet/files/kinetics_600_labels.txt;官方 Colab 教程 movinet_tutorial.ipynb 与流式训练/推理教程 movinet_streaming_model_training_and_inference.ipynb 覆盖了完整操作路径,tools/plot_movinet_video_stream_predictions.ipynb 则用于生成视频预测曲线图。
部署到移动端:TF Lite 导出全流程
本节完整继承 README "TF Lite Example" 的流程,共三步:(可选)3D 权重转换、导出 SavedModel、转换并运行 TF Lite。
第一步(可选):3d_2plus1d 权重转 2plus1d 图
流式模型训练时通常用conv_type='3d_2plus1d'以获得更高训练吞吐。要在 CPU 上获得更好的推理性能,需把权重转换到2plus1d图结构,仓库提供现成工具 tools/convert_3d_2plus1d.py(配套测试见 tools/convert_3d_2plus1d_test.py)。若你的 checkpoint 本身就是2plus1d训练的,可跳过此步。
第二步:导出 TF SavedModel
以 MoViNet-A0-Stream 为例,运行 tools/export_saved_model.py:
python3 export_saved_model.py \ --model_id=a0 \ --causal=True \ --conv_type=2plus1d \ --se_type=2plus3d \ --activation=hard_swish \ --gating_activation=hard_sigmoid \ --use_positional_encoding=False \ --num_classes=600 \ --batch_size=1 \ --num_frames=1 \ --image_size=172 \ --bundle_input_init_states_fn=False \ --checkpoint_path=/path/to/checkpoint \ --export_path=/tmp/movinet_a0_stream参数说明(对照 configs/movinet.py 中Movinet配置类的默认值):
--model_id:a0~a5,对应BLOCK_SPECS中的架构;--causal=True、--conv_type=2plus1d、--se_type=2plus3d:流式模型三要素,与上文"架构细节"一致;--activation=hard_swish与--gating_activation=hard_sigmoid:流式 checkpoint 使用的硬件友好激活(配置类默认值为swish/sigmoid,Base 模型训练即使用默认值);--use_positional_encoding:仅 a3/a4/a5 需要置 True;--batch_size=1 --num_frames=1 --image_size=172:移动端单帧推理规格;--bundle_input_init_states_fn:控制是否在 SavedModel 中附带生成初始状态的函数;--num_classes:600 对应 Kinetics 600,400 对应 Kinetics 400。
第三步:转换为 TF Lite 并用 Interpreter 逐帧运行
saved_model_dir = '/tmp/movinet_a0_stream' converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) tflite_model = converter.convert() with open('/tmp/movinet_a0_stream.tflite', 'wb') as f: f.write(tflite_model)推理时,SavedModel 的每个状态输入都有一个serving_default_...签名键,需要还原为状态名并初始化为 0 张量,然后逐帧调用 signature runner:
# Create the interpreter and signature runner interpreter = tf.lite.Interpreter('/tmp/movinet_a0_stream.tflite') runner = interpreter.get_signature_runner() # Extract state names and create the initial (zero) states def state_name(name: str) -> str: return name[len('serving_default_'):-len(':0')] init_states = { state_name(x['name']): tf.zeros(x['shape'], dtype=x['dtype']) for x in interpreter.get_input_details() } del init_states['image'] # Insert your video clip here video = tf.ones([1, 8, 172, 172, 3]) clips = tf.split(video, video.shape[1], axis=1) # To run on a video, pass in one frame at a time states = init_states for clip in clips: # Input shape: [1, 1, 172, 172, 3] outputs = runner(**states, image=clip) logits = outputs.pop('logits') states = outputs注意init_states的构造方式:image是普通输入而非状态,需要删除;其余输入键都是状态(流缓冲区、累积池化缓冲、帧计数),初始值全部为 0,这与 Python 端model.init_states()的语义完全对应。仓库另外提供量化脚本 tools/quantize_movinet.py,README 提到的"量化 TF Lite 二进制"即由此类流程产出。部署到真实移动设备请遵循 TensorFlow Lite 官方指南。
训练与评测:配置驱动的官方流程
训练与评测命令
入口脚本 official/projects/movinet/train.py 基于官方train_lib.run_experiment框架:先解析 YAML 配置与 Gin 绑定,设置混合精度策略(bfloat16用于 TPU、mixed_float16用于 GPU),再按--mode分派训练/评测。完整命令(继承自 README):
MODE=train_and_eval # Can also be 'train' if using a separate evaluator job CONFIG_FILE=official/projects/movinet/configs/yaml/movinet_a0_k600_8x8.yaml python3 official/projects/movinet/train.py \ --experiment=movinet_kinetics600 \ --mode=${MODE} \ --model_dir=/tmp/movinet_a0_base/ \ --config_file=${CONFIG_FILE}纯评测使用MODE=eval(训练期间持续评测则用eval_continuous),命令结构相同。--experiment=movinet_kinetics600对应 configs/movinet.py 中经@exp_factory.register_config_factory('movinet_kinetics600')注册的经验工厂函数,它复用video_classification_kinetics600()并强制训练/验证数据 dtype 为bfloat16。
关键训练配置逐项解读
以官方 A0/Kinetics-600 TPU 配置 configs/yaml/movinet_a0_k600_8x8.yaml 为例(文件头注明该配置达到 72.28% Top-1,与 README 表格一致):
- runtime:
distribution_strategy: tpu、mixed_precision_dtype: bfloat16——8x8 TPU pod 的混合精度训练;同目录还有movinet_a0_gpu.yaml(GPU 版)与movinet_a0_k600_cpu_local.yaml(CPU 本地调试版); - 数据:
feature_shape: [50, 172, 172, 3]、temporal_stride: 5、random_stride_range: 1——50 帧、时间步长 5,即约 10 秒片段;global_batch_size: 1024;增强为aug_type: autoaug,并限定裁剪面积比 0.08~1.0、宽高比 0.5~2.0、min_image_size: 192; - 模型:
model_id: a0、stochastic_depth_drop_rate: 0.2(随机深度按层线性递增,见 movinet.py 第 486-488 行:drop_rate * idx / num_layers)、dropout_rate: 0.2、use_sync_bn: true、激活swish; - 损失:
l2_weight_decay: 0.00003、label_smoothing: 0.1; - 优化器:RMSProp(
rho: 0.9, momentum: 0.9, epsilon: 1.0, clipnorm: 1.0),学习率为余弦衰减(初值 1.8,decay_steps: 85785)加线性 warmup(2145 步),总步数train_steps: 85785; - 验证:
num_test_clips: 1、num_test_crops: 1,与 README"单片段、训练分辨率"的评测口径一致。
configs/yaml 目录下为 A0~A5 各提供 Base(*_k600_8x8.yaml)与 Stream(*_stream_k600_8x8.yaml)两套 TPU 配置,另有 T0 的两套配置;修改模型规模时只需替换--config_file即可。测试侧可用 configs/movinet_test.py、train_test.py 以及 modeling/movinet_test.py、modeling/movinet_model_test.py 验证配置解析与建图逻辑。
版本演进、许可与引用
README 的 History 记录了模块演进:2021-05-11初始提交;2021-05-30增加流式 MoViNet checkpoint 与示例;2021-07-12增加 TF Lite 支持,并把 3D 流式模型替换为对移动端更友好的 (2+1)D 流式模型(这解释了为何流式模型采用2plus1d而非3d);2022-03-14支持量化 TF Lite 模型并更新 Colab notebook。
该模块要求 TensorFlow 2.4 及以上、Python 3.6 及以上环境,依赖清单见 official/projects/movinet/requirements.txt。整个 MoViNet 模块遵循Apache License 2.0许可。若在论文中引用本实现,请使用:
@article{kondratyuk2021movinets, title={MoViNets: Mobile Video Networks for Efficient Video Recognition}, author={Dan Kondratyuk, Liangzhe Yuan, Yandong Li, Li Zhang, Matthew Brown, and Boqing Gong}, journal={arXiv preprint arXiv:2103.11511}, year={2021} }小结
MoViNets 的价值在于用一套统一的状态化因果架构同时覆盖了"离线批量评测"与"端上流式实时识别"两种场景:Base 模型用标准 3D 卷积换取简单性,Streaming 模型用 (2+1)D 因果卷积加显式状态字典(流缓冲区、累积池化缓冲、帧计数)把任意长视频变成逐帧可增量计算的过程。结合仓库中的预训练权重、TF Lite 导出工具链(convert_3d_2plus1d.py→export_saved_model.py→TFLiteConverter)以及配置驱动的训练入口 train.py,开发者可以在 2.7 GFLOPs 的 A0 到 280 GFLOPs 的 A5 之间按部署预算灵活选型,并把整条"训练—评测—导出—端侧推理"链路全部跑通。
【免费下载链接】modelsModels and examples built with TensorFlow项目地址: https://gitcode.com/GitHub_Trending/mode/models
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考