反应视频25实战:用CNN+LSTM实现视频人物反应识别 前一段时间团队接了一个“用户测评视频自动分析”的需求给一批用户看完产品介绍视频后的前置录像让系统自动判断用户当时的反应状态——是惊喜、困惑、走神还是完全无感。一开始大家觉得不复杂把视频交给外包标注团队人工打标签就行。可真正跑起来才发现几十个小时的视频文件光看完一遍都要好几个工作日更别说标注标准还不统一。这时我们才意识到这类任务的本质不是“看视频”而是“视频理解”是需要用模型把人物反应自动识别出来的工程问题。这篇文章要聊的就是把“反应视频”当成一个可落地的识别项目来做的方法。我把它命名为“反应视频25”对应一个包含 25 类人物反应标签的视频分类任务。文章会从问题定义、核心原理、数据准备、模型选型、训练推理到工程排查完整走一遍流程。核心判断是这类视频理解任务并不需要一开始就上大规模视频预训练大模型用“预训练 CNN 抽取关键帧特征 轻量时序模型聚合”的组合就能在中小规模数据上得到一条可用的基线。先跑通基线再谈复杂模型是性价比最高的路径。1. 反应视频识别到底要解决什么问题1.1 你说的是哪种“反应视频”在正式开始前先做一个概念澄清。在视频平台语境下“反应视频”通常指一类内容形式——创作者录制自己观看某个视频时的表情和即时反馈然后把“原始视频画面”和“观看者画面”放在同一个画面里。这种内容形式在测评、综艺点映、游戏试玩等领域非常常见。但这篇文章要讲的不是如何剪辑这类视频而是反过来**如何用计算机视觉和深度学习模型自动识别视频里的人做出的反应是什么。**换句话说我们要把“人看到内容后的表情、动作、情绪状态”作为一个可分类的信号用模型完成分类。这个任务在学术上更接近视频行为识别Video Action Recognition和动态表情识别Dynamic Facial Expression Recognition的交叉方向但在工程上它有很强的落地价值这也是我决定把它写成一篇 CSDN 技术长文的原因。1.2 典型业务场景如果你觉得“识别人的反应”听起来很学术可以先看看下面几个真实存在的业务场景直播或点播场景下的观众情绪分析。平台希望统计用户在看某个节目片段时的高光时刻比如哪一段让观众普遍笑了哪一段让观众走了神。用户调研与可用性测试。产品团队让用户录制使用 App 时的面部反应用于分析哪些页面让用户困惑哪些流程让用户满意。在线教育。教育机构用视频分析学生上课时的专注程度判断课程内容的节奏是否合适。内容审核与质量评估。内容生产者希望了解自己的成片在哪一秒抓住了观众在哪一秒开始流失注意力。这些场景有一个共同点**数据都是短视频片段分析对象都是画面里的人物状态最终产品都需要一个自动化管线。**人工看视频分析不是不行但规模一上来成本和时间都不可控。于是“用模型识别反应”就成了必须做的工程改造。1.3 这背后真正的技术难点如果你只看“分类”两个字会觉得这就是个图像分类问题把每一帧图片丢给 ResNet输出一个 25 类的概率分布。但视频不是图片。一个关键差异在于反应是随时间展开的过程。“从平静到惊讶”和“一直处于惊讶状态”是两种完全不同的反应但单帧特征可能非常相似。模型需要理解时序上下文才能区分“惊喜”和“惊吓”区分“疑惑”和“走神”。所以这个任务的核心挑战可以拆成三个空间特征单帧画面里的人脸表情、身体姿态、互动对象是谁。时序特征这些空间特征在一段时间内如何变化。模态融合在很多场景下我们还希望加入语音特征比如观众的笑声、惊叹声甚至是自言自语的声音。这也决定了技术路线不能只用一个图像分类模型还需要引入时间维度的建模手段。2. 核心概念帧、时序建模与多模态融合2.1 视频 帧序列 声音轨在模型眼里一段 mp4 文件并不是“一段视频”而是一个由连续图片组成的序列以及一条和图片时间轴对齐的音频流。如果我们的目标是识别人物反应最直接的输入就是视觉帧序列。但一刀切不做任何压缩直接把每秒 25 帧、每帧 1080p 的原始像素丢进模型显存会立刻爆炸。因此工程上必须要做两件事降采样按固定间隔抽取关键帧比如每秒抽 2 到 5 帧。缩放把每一帧缩放到模型输入要求的尺寸常见的是 224×224 或者 112×112。你可能担心抽帧会丢失信息。确实会但我们建模的是“反应”这种宏观状态不是细微的表情肌肉运动低帧率加适当分辨率已经能覆盖大部分区分性信息。2.2 空间特征提取空间特征指的是“这一帧画面里有什么”。目前最稳定的做法是使用在大规模图像数据集上预训练过的卷积神经网络CNN例如 ResNet、EfficientNet、ConvNeXt。让 CNN 输出一个特征向量代表这一帧的内容。这里有一个容易误解的地方我们究竟应该用 CNN 直接输出分类结果还是输出特征答案是后者。在视频任务里CNN 通常被当作“特征提取器”而不是最终分类器。真正做时序聚合的是后面的模型。2.3 时序建模拿到每一帧的特征向量后就需要按时间顺序把它们组合起来。常见的方案有以下几种。方案思路优点缺点时序平均/最大池化把所有帧特征取平均或最大值简单、稳定、不易过拟合丧失顺序关系不能区分“先笑后哭”和“先哭后笑”LSTM/GRU用循环神经网络顺序读取帧特征能建模顺序信息训练相对慢对长序列效果下降1D 卷积把帧特征序列看作一维信号用卷积核提取局部时序模式速度快、可并行对超长依赖建模能力有限Transformer用自注意力建模任意帧之间的关系表达能力最强数据量小容易过拟合训练成本高对“反应视频 25”这个场景如果你数据量不大我建议先跑通前两种拿到基线再做 Transformer 也不迟。2.4 多模态要不要加语音很多时候人物反应的判断依据不只有表情还有声音。一个惊喜反应往往会伴随着“哇”的一声。一个惊悚反应可能伴随尖叫。从工程角度语音信息处理成本比视觉高不少。首先需要用语音活动检测VAD切除静音段然后要选择音频特征提取方式比如预训练的 Wav2Vec2或者传统的 Mel 频谱图加 CNN。这会让项目复杂度上一个台阶。因此我的建议是**第一版先只做视觉模型。**等视觉模型效果不够用了再考虑把音频特征和视频特征在模型倒数第二层做拼接融合。这种增量式的多模态接入方式在项目推进过程中最稳妥。3. 技术选型与环境准备3.1 环境清单本文示例代码使用 Python 与 PyTorch 实现这也是视频理解任务目前最主流的技术栈。版本细节以你实际安装为准本文演示的是通用实现思路。需要准备的环境组件如下组件说明Python建议使用 3.9 或更高版本PyTorch建议使用 1.13 或更高版本包含 torchvisionOpenCV用于视频读取和图像处理NumPy数据处理与数组运算tqdm训练进度条显示TensorBoard 或 wandb训练指标可视化可选3.2 安装依赖在命令行中执行以下命令创建虚拟环境并安装依赖。python -m venv venv source venv/bin/activate # Windows 下使用 venv\\Scripts\\activate pip install --upgrade pip pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python numpy tqdm如果你的机器没有 NVIDIA GPU可以把最后一行的 torch 安装命令替换为 CPU 版本去掉--index-url即可。但后面训练速度会比较慢建议至少有一块 8 GB 以上显存的 GPU。3.3 为什么选择 PyTorch相比 TensorFlowPyTorch 在视频理解和多模态领域的社区示例更多封装方式更贴近 Python 习惯也更容易在 debug 时看到中间张量的变化。对本文这种“自定义数据集 自定义训练循环”的中小型项目PyTorch 的灵活性是最好的。4. 数据准备与预处理4.1 数据目录结构假设我们已经收集了一批带标签的短视频每个视频对应一个反应类别。类别总数设定为 25 个。为了保持代码简洁建议按下面的目录结构组织原始数据reaction_video_25/ ├── data/ │ ├── raw/ │ │ ├── happy/ │ │ │ ├── 001.mp4 │ │ │ ├── 002.mp4 │ │ │ └── ... │ │ ├── surprise/ │ │ │ └── ... │ │ └── ... ├── scripts/ │ ├── extract_frames.py │ ├── dataset.py │ ├── model.py │ ├── train.py │ └── predict.py └── runs/每个二级目录名就是类别名。这样我们在读取数据时可以直接通过父目录名得到标签。4.2 关键帧抽取脚本原始视频通常时长不一而且分辨率较高。我们先用 OpenCV 把每个视频抽成固定数量的帧。为什么要固定数量因为后续模型需要把不同视频表示成同样大小的张量才能在一个 batch 里训练。下面是一个示例脚本# 文件路径scripts/extract_frames.py import os import cv2 import numpy as np def extract_frames(video_path, target_frame_count16, target_size(224, 224)): cap cv2.VideoCapture(video_path) if not cap.isOpened(): raise RuntimeError(f无法打开视频文件: {video_path}) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if total_frames 0: cap.release() raise RuntimeError(f视频帧数为 0: {video_path}) # 在时间轴上均匀抽取 target_frame_count 帧 frame_indices np.linspace(0, total_frames - 1, target_frame_count, dtypeint) frames [] for idx in frame_indices: cap.set(cv2.CAP_PROP_POS_FRAMES, idx) ret, frame cap.read() if not ret: # 如果读取失败用黑色帧补位 frame np.zeros((target_size[1], target_size[0], 3), dtypenp.uint8) frame cv2.resize(frame, target_size) frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frames.append(frame) cap.release() return np.stack(frames) # shape: (target_frame_count, 224, 224, 3) if __name__ __main__: sample_frames extract_frames(data/raw/happy/001.mp4) print(抽取结果形状:, sample_frames.shape)这个脚本会输出类似下面的结果抽取结果形状: (16, 224, 224, 3)表示我们得到了 16 帧、每帧 224×224 的 RGB 图像。如果视频本身很短帧数不足 16 帧上面的代码会重复读取最后一帧或补黑帧。更稳妥的做法是先把所有帧读到内存再做均匀抽样但要额外注意内存占用。4.3 数据加载器数据加载器负责在训练过程中动态读取视频、抽帧、返回标签。我们直接基于 PyTorch 的Dataset类实现。# 文件路径scripts/dataset.py import os import torch from torch.utils.data import Dataset from extract_frames import extract_frames class ReactionVideoDataset(Dataset): def __init__(self, root_dir, class_names, target_frame_count16, target_size(224, 224)): self.samples [] self.target_frame_count target_frame_count self.target_size target_size self.class_to_id {name: idx for idx, name in enumerate(class_names)} for class_name in class_names: class_dir os.path.join(root_dir, class_name) if not os.path.isdir(class_dir): continue for video_name in os.listdir(class_dir): if not video_name.lower().endswith((.mp4, .avi, .mov)): continue video_path os.path.join(class_dir, video_name) self.samples.append((video_path, self.class_to_id[class_name])) def __len__(self): return len(self.samples) def __getitem__(self, index): video_path, label self.samples[index] frames extract_frames(video_path, self.target_frame_count, self.target_size) # 将 (T, H, W, C) 转换为 (C, T, H, W) 并归一化 frames torch.from_numpy(frames).permute(3, 0, 1, 2).float() / 255.0 return frames, torch.tensor(label, dtypetorch.long)在__getitem__里我们之所以实时抽帧是为了减少磁盘占用。如果你有足够的内存空间也可以提前把所有视频的帧抽取到.npy文件里训练时会快很多。4.4 训练集和验证集划分不要把所有视频都拿去训练。建议按照 8:2 的比例划分训练集和验证集并且要保证同一个视频的片段不会同时出现在训练集和验证集中。否则会出现数据泄漏验证集指标虚高。# 在项目根目录执行 python -c import os, random from shutil import copy2 random.seed(42) raw_dir data/raw train_dir data/train val_dir data/val for class_name in os.listdir(raw_dir): class_path os.path.join(raw_dir, class_name) if not os.path.isdir(class_path): continue videos os.listdir(class_path) random.shuffle(videos) split_idx int(len(videos) * 0.8) train_videos videos[:split_idx] val_videos videos[split_idx:] # 这里只打印路径实际项目请按需复制或生成索引文件 print(class_name, train:, len(train_videos), val:, len(val_videos)) 在实际项目里不建议直接复制大量视频文件更推荐生成一个记录视频路径和标签的 CSV 索引文件然后用索引文件筛选训练集和验证集。5. 模型训练代码实现5.1 模型设计考虑到视频数据量通常不大我们采用“预训练 CNN 特征提取 时序聚合”设计。这里给出两个可选方案。方案 A预训练 CNN 全局平均池化。最简单作为一个强基线。方案 B预训练 CNN LSTM。在方案 A 的基础上加入顺序信息适合“反应视频 25”这种需要区分时序变化的场景。下面以方案 B 为例给出模型代码。# 文件路径scripts/model.py import torch import torch.nn as nn import torchvision.models as models class VideoReactionModel(nn.Module): def __init__(self, num_classes25, lstm_hidden_size256, pretrainedTrue): super().__init__() # 使用预训练 ResNet18 作为空间特征提取器 resnet models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1 if pretrained else None) # 去掉最后的全连接分类层 self.feature_extractor nn.Sequential(*list(resnet.children())[:-1]) # 得到每个视频的特征维度ResNet18 是 512 feature_dim 512 self.lstm nn.LSTM( input_sizefeature_dim, hidden_sizelstm_hidden_size, num_layers1, batch_firstTrue ) self.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(lstm_hidden_size, num_classes) ) def forward(self, x): # x: (batch_size, C, T, H, W) batch_size, C, T, H, W x.shape # 把时间维度和 batch 合并方便 CNN 处理 x x.permute(0, 2, 1, 3, 4).reshape(batch_size * T, C, H, W) features self.feature_extractor(x) # (batch_size * T, 512, 1, 1) features features.view(batch_size, T, -1) # (batch_size, T, 512) lstm_out, _ self.lstm(features) # (batch_size, T, hidden_size) # 取最后一个时间步作为序列表示 last_out lstm_out[:, -1, :] # (batch_size, hidden_size) logits self.classifier(last_out) # (batch_size, num_classes) return logits这段代码的核心逻辑是输入张量形状为(batch, channel, time, height, width)。先把时间和 batch 合并一次性让 ResNet18 处理全部帧提高并行效率。将 ResNet18 输出恢复成(batch, time, feature_dim)。送入 LSTM取最后一个时间步的输出再经过全连接层得到 25 类 logits。这里有一点需要注意取 LSTM 最后一个时间步意味着我们只关心序列末端的整体状态。如果你觉得最后一步丢失了早期信息可以考虑改成对所有时间步做平均池化或者用注意力机制加权求和。5.2 训练循环训练脚本需要完成数据加载、损失计算、梯度更新、验证集评估这几件事。# 文件路径scripts/train.py import os import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms from dataset import ReactionVideoDataset from model import VideoReactionModel def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) print(使用设备:, device) class_names [happy, surprise, confusion, boredom, anger, fear, disgust, sadness, excitement, neutral, amusement, curiosity, frustration, relief, satisfaction, disappointment, admiration, awe, embarrassment, interest, joy, anxiety, calmness, skepticism, approval] train_dataset ReactionVideoDataset(data/train, class_names) val_dataset ReactionVideoDataset(data/val, class_names) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size8, shuffleFalse, num_workers2) model VideoReactionModel(num_classeslen(class_names)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) num_epochs 20 for epoch in range(num_epochs): model.train() total_loss 0.0 for frames, labels in train_loader: frames frames.to(device) labels labels.to(device) logits model(frames) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * frames.size(0) avg_train_loss total_loss / len(train_dataset) # 验证集准确率 val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1}/{num_epochs}, Train Loss: {avg_train_loss:.4f}, Val Acc: {val_acc:.4f}) scheduler.step() def evaluate(model, loader, device): model.eval() correct 0 total 0 with torch.no_grad(): for frames, labels in loader: frames frames.to(device) labels labels.to(device) logits model(frames) preds logits.argmax(dim1) correct (preds labels).sum().item() total labels.size(0) return correct / total if total 0 else 0.0 if __name__ __main__: main()训练时建议开启终端输出每跑一个 epoch 都观察训练损失和验证准确率。如果训练损失持续下降但验证准确率不涨基本可以判断已经过拟合需要加正则化或数据增强。5.3 运行训练在项目根目录执行python scripts/train.py预期会出现类似下面的日志使用设备: cuda Epoch 1/20, Train Loss: 2.8731, Val Acc: 0.1052 Epoch 2/20, Train Loss: 2.4120, Val Acc: 0.1819 Epoch 3/20, Train Loss: 2.0054, Val Acc: 0.2447 ... Epoch 20/20, Train Loss: 0.6432, Val Acc: 0.4721注意这里的数字不是固定结果它会受数据量、数据分布、随机种子和超参数的影响。如果验证集准确率显著高于随机水平25 类随机猜测约 4%说明模型已经学到了一些规律如果准确率很低先检查数据质量和标签是否对齐。6. 推理与效果验证6.1 单视频预测脚本训练完成后我们会保存一份模型权重文件。下面给出一个推理脚本读取一个新的视频文件输出 25 个类别的预测概率。# 文件路径scripts/predict.py import torch from extract_frames import extract_frames from model import VideoReactionModel class_names [happy, surprise, confusion, boredom, anger, fear, disgust, sadness, excitement, neutral, amusement, curiosity, frustration, relief, satisfaction, disappointment, admiration, awe, embarrassment, interest, joy, anxiety, calmness, skepticism, approval] def predict_video(video_path, model_path, devicecuda): model VideoReactionModel(num_classeslen(class_names)) state_dict torch.load(model_path, map_locationdevice) model.load_state_dict(state_dict) model.to(device) model.eval() frames extract_frames(video_path) # 增加 batch 维度: (1, C, T, H, W) input_tensor torch.from_numpy(frames).permute(3, 0, 1, 2).float().unsqueeze(0) / 255.0 input_tensor input_tensor.to(device) with torch.no_grad(): logits model(input_tensor) probs torch.softmax(logits, dim1).squeeze(0) top_probs, top_indices torch.topk(probs, k5) print(f视频: {video_path}) for idx, prob in zip(top_indices.tolist(), top_probs.tolist()): print(f{class_names[idx]:15} {prob:.4f}) if __name__ __main__: predict_video(data/raw/surprise/001.mp4, runs/best_model.pth)运行后输出示例视频: data/raw/surprise/001.mp4 surprise 0.6832 excitement 0.1217 interest 0.0931 awe 0.0524 amusement 0.0115如果 top1 类别和我们人工判断一致说明模型已经具备了基本的反应识别能力。6.2 如何判断模型效果是否合格准确率不是唯一指标。对于 25 分类问题必须关注混淆矩阵。比如“excitement”和“surprise”很容易混淆“boredom”和“calmness”也很容易混淆这不一定说明模型不行也可能说明两个类别本身在视觉上边界模糊。所以验证阶段建议额外做三件事输出每个类别的 precision、recall、F1-score。观察混淆矩阵找出最容易互相混的类别对。抽 20 到 50 个预测错误的样本人工复核视频内容判断是标签错误还是模型错误。如果你发现模型把大量真实“surprise”预测成了“excitement”而人工看视频也确实很难区分那要考虑合并类别或者补充更多带标注的数据。7. 常见问题与排查思路实际开发过程中“反应视频 25”这类项目最容易踩的坑不在模型结构而在数据、显存和训练稳定性。我把高频问题整理成了一张表。问题现象可能原因排查方式解决方案训练时显存溢出batch size 太大输入帧数太多查看报错堆栈中的张量形状减小 batch size或者降低 target_frame_count 和分辨率模型输出类别全一样数据集类别严重不均衡打印训练集各类别数量使用加权采样器或类别平衡损失函数验证集准确率远低于训练集过拟合对比训练和验证 loss增加数据增强、Dropout或减小模型规模视频读取失败或帧数为 0视频编码格式问题用播放器打开原视频确认编码用 ffmpeg 统一转码为 H.264 的 mp4CPU 训练极慢机器没有可用的 GPU运行nvidia-smi检查驱动安装 GPU 版 PyTorch或减少数据量先跑通逻辑全部视频预测为同一个类模型没有真正收敛或标签错位先检查训练集的样本数和标签路径检查 Dataset 的 class_to_id 映射是否正确视频首尾帧和内容无关抽帧选择了黑帧或转场帧人工抽查抽取的关键帧在抽帧逻辑中过滤灰度方差过低的帧在这些问题里最容易被忽略的是“标签错位”。如果class_names的顺序与训练时的顺序不一致模型加载权重后预测结果会整体偏移。建议把class_names保存为 JSON 文件与模型权重一起导出。{ class_names: [happy, surprise, confusion], target_frame_count: 16 }这样在推理阶段直接读取就不会因为手工维护类别列表顺序出错。8. 最佳实践与工程建议8.1 先定最小可行版本再逐步加复杂度“反应视频 25”这类项目常见的失败原因不是模型不够强而是第一次迭代就想做到完美。上来就上 Video Transformer数据只有几百条效果反而不如简单模型。更推荐的做法是用 ResNet18 时序平均池化跑通第一条基线。记录准确率、混淆矩阵和错误案例。更换为 LSTM 或 Transformer 结构对比是否真的有提升。最后再考虑是否加入音频、人脸关键点等额外信息。每一步只改一个变量方便定位效果差异来自哪里。8.2 数据质量大于模型结构从工程经验看这类任务的性能上限往往由数据质量决定。反应标签是主观性很强的标注任务不同标注者对“confusion”和“boredom”的判断可能完全不一致。建议在标注阶段做两件事每个视频至少由两名标注人员独立标注出现不一致时由第三人裁决。定期抽样做标注一致性检查计算 Cohens Kappa 系数。如果标注一致性过低先统一标注规则再训练模型。8.3 保持随机种子固定视频模型训练过程受权重初始化、数据加载顺序影响很大。为了复现实验结果必须在代码中固定随机种子。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)在训练脚本入口第一行调用这个函数。8.4 模型导出与部署训练好的 PyTorch 模型如果直接用于生产服务需要考虑推理性能和依赖问题。常见的做法是把模型导出为 TorchScript 或 ONNX 格式再用 ONNX Runtime 推理。导出 ONNX 的关键是要处理好动态轴。视频输入的时间维度和 batch 维度都是变化的导出时需要指定动态轴model.eval() dummy_input torch.randn(1, 3, 16, 224, 224) torch.onnx.export( model, dummy_input, reaction_video_25.onnx, input_names[video], output_names[logits], dynamic_axes{video: {0: batch, 2: time}} )这样导出后模型就支持不同 batch 和不同帧数的输入适合接入后端服务。8.5 注意数据采集与隐私合规反应视频通常包含真实人物的面部画面。如果项目用于商业场景要特别注意数据授权和隐私合规。建议在数据采集阶段获得明确的视频使用授权并在脱敏处理、存储加密、访问审计等环节建立基本规范。技术上也可以考虑在训练和推理时对人脸区域做局部模糊降低隐私风险。9. 总结与后续学习方向这篇文章从“反应视频 25”这个项目概念出发拆解了把“人物反应视频识别”做成可落地工程的关键路径。我们讨论了三层核心问题空间特征提取怎么做、时序信息怎么建模、数据准备和工程验证怎么开展并给出了从抽帧、数据加载、模型训练到单视频预测的完整代码示例。如果你正准备做类似的视频理解项目第一步一定是先建立一套干净、可复现的数据预处理管线然后用最简单的模型跑通基线。在这个基础上把混淆矩阵、错误案例、标注一致性这些工程指标补上再决定是否引入更复杂的时序模型或多模态特征。后续值得深入的方向包括Video Transformer 在小型数据集上的迁移学习、大语言模型驱动的视频描述生成、以自监督方式利用无标签视频提升特征质量。但无论选择哪个方向“先跑通、再优化、每次只改一个变量”这个原则都不会变。