视频理解模型推理与微调
视频理解模型推理与微调随着多模态大模型的持续发展视频理解、多模态检索和智能标注等应用场景逐渐落地。为了进一步探究多模态视频理解模型在实际工程中的应用方式本文以 Qwen3-VL 系列模型为例系统梳理其在视频场景下的推理与全参数微调实践。文章首先通过一个完整的视频本地推理示例详细拆解模型输入的构建流程包括视频帧采样、patch 划分以及时序与空间维度的合并等关键细节随后基于真实游戏视频数据介绍视频指令微调数据集的构建方法并完成一次全参数微调与效果验证。回到顶部1. 视频推理首先看一个qwen3-vl-4b-instruct模型做视频本地推理的例子示例代码如下123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899fromtransformersimportAutoProcessor, AutoModelForVision2Seqfromqwen_vl_utilsimportprocess_vision_infoimportwarningswarnings.filterwarnings(ignore, categoryFutureWarning, moduletransformers)importos# 加载模型和处理器model_pathQwen/Qwen3-VL-4B-InstructprocessorAutoProcessor.from_pretrained(model_path)# 加载视觉-文本模型model, output_loading_infoAutoModelForVision2Seq.from_pretrained(model_path,torch_dtypeauto,device_mapauto,output_loading_infoTrue)print(output_loading_info, output_loading_info)# 配置视频处理参数video./1.mp4total_pixels20480*32*32# 总像素限制min_pixels64*32*32# 最小像素限制max_frames2048# 最大帧数sample_fps2# 采样帧率max_new_tokens2048# 生成的最大 token 数prompt请描述这个视频的内容# 构建对话消息messages[{role:user,content: [{video: video,total_pixels: total_pixels,min_pixels: min_pixels,max_frames: max_frames,sample_fps: sample_fps},{type:text,text: prompt},]},]# 应用对话模板textprocessor.apply_chat_template(messages, tokenizeFalse, add_generation_promptTrue)# 处理视觉信息image_inputs, video_inputs, video_kwargsprocess_vision_info([messages],return_video_kwargsTrue,image_patch_size16,return_video_metadataTrue)# 分离视频数据和元数据ifvideo_inputsisnotNone:video_inputs, video_metadataszip(*video_inputs)video_inputs, video_metadataslist(video_inputs),list(video_metadatas)else:video_metadatasNone# 准备模型输入inputsprocessor(text[text],imagesimage_inputs,videosvideo_inputs,video_metadatavideo_metadatas,**video_kwargs,do_resizeFalse,return_tensorspt)inputsinputs.to(cuda)# 生成输出output_idsmodel.generate(**inputs, max_new_tokensmax_new_tokens)# 提取生成的部分去除输入generated_ids[output_ids[len(input_ids):]forinput_ids, output_idsinzip(inputs.input_ids, output_ids)]# 解码为文本output_textprocessor.batch_decode(generated_ids,skip_special_tokensTrue,clean_up_tokenization_spacesTrue)print(output_text)[视频由多个镜头组成展现了动漫风格的画面和人物。第一部分是一个男性角色的特写他有着黑色长发额头中央有一道红色印记眼神锐利手指轻触脸颊表情严肃。画面中出现字幕“顾长歌真是嘎嘎坏”“几百岁的老女人的主意他也打”“就在顾长歌陷害唐天后”。这暗示了角色顾长歌的狡猾和他所策划的阴谋。\n\n第二部分切换到一个女性角色的特写她有着长长的黑发眉目间流露出一丝冷笑嘴角上扬似乎在策划或执行某个计划。字幕显示“他吩咐尹湄”“让唐天的姐姐唐婉亲自过来赎人”。这表明她被顾长歌指派去执行某个任务。\n\n第三部分是另一个女性角色的特写她的紫色眼睛非常醒目眼神中透露出惊讶和震惊。字幕显示“唐婉有着一门预知祸福的术法神通”“当她用这种术法”“对这次赎人之约推演时”“却意外的撞到了城”。这说明唐婉拥有某种预知能力但在推演时却意外地遇到了什么暗示了剧情的转折。\n\n整个视频通过这些特写镜头展现了角色之间的复杂关系和剧情的发展充满了紧张和悬念。]1.1. 模型输入说明这里着重注意一下模型的输入部分处理。可以看到在构建inputs时需要有几个输入分别为[text]、image_inputs、video_inputs、video_metadatas和video_kwargs以及do_resizeFalse。首先text是对话模板其对应的内容为1|im_start|user\n|vision_start||video_pad||vision_end|请描述这个视频的内容|im_end|\n|im_start|assistant\n可以看到text定义了后续输入到模型的模板其中video_pad即为占位token后续将由实际的video数据进行补充。1.2. 视频帧信息准备image_inputs, video_inputs和video_kwargs为 process_vision_info() 方法处理后的返回。这个方法是将原始视频文件转换为模型可以理解的格式具体地说会做以下几件事情使用opencv读取视频基本信息包括视频的fps假设是60 fps, total_frames假设视频为15s则为900帧, width1280和height720按照指定的sample_fps进行帧采样这里指定为2也就是每1s采2帧。原视频为15秒900帧的话在采样后就是30帧采样间隔就是60/230也就是每30帧取1帧。这样就得到了后续用于推理的帧序列0, 31, 62, …, 899动态调整分辨率。在传入的messages参数里除了video的路径外还包括, min_pixels指定为64 * 32 * 32 65,536, total_pixels指定为20480 * 32 * 3220,971,520 和 sample_fps的信息。sample_fps的作用上面已经提到。total_pixels和min_pixels用于约束调整分辨率。假设原始视频是1280 x 720的分辨率也就是每帧包含921,600个像素。那30帧 * 921,600 27,648,000总像素大于指定的max_pixels。所以会继续对帧序列进行像素缩放调整到patch_size的整数倍。Patch size是ViTVision Transformer架构的核心设计它是将图像切分为固定大小的patch每个patch最终作为1个token输入到transformer中。这里patch size指定为16也就是每个token本质上是一个16x16的像素区域。所以需要基于patch_size的设置将每帧调整到patch_size的整数倍对齐到patch_size的整数倍后最终分辨率为1152 x 640最后将帧列表也就是抽取的30帧做了分辨率调整做了基于patch_size调整后的帧序列转为PyTorch张量并返回。最后可以看到video_inputs的结果为单元素的list这单个元素里为1个tuple包含2部分一个shape为([30, 3, 640, 1152]) 的张量分别代表帧id、RGB通道、缩放后的分辨率640 x 1152另一个为视频的metadata信息包括fps和frame_indices采样后的帧序列以及total_num_frames所以在后续代码中还有一步是123ifvideo_inputsisnotNone:video_inputs, video_metadataszip(*video_inputs)video_inputs, video_metadataslist(video_inputs),list(video_metadatas)这是将video_inputs和video_metadatas拆开在video_inputs中只保留帧的信息。1.3. 模型输入信息准备在视频帧信息准备好之后还有最后一步将视频帧信息添加到text模板中也就是这段代码1inputsprocessor(text[text], imagesimage_inputs, videosvideo_inputs, video_metadatavideo_metadatas,**video_kwargs, do_resizeFalse, return_tensorspt)此时输入数据为text |im_start|user\n|vision_start||video_pad||vision_end|请描述这个视频的内容|im_end|\n|im_start|assistant\nvideo_inputs [torch.Tensor([30, 3, 640, 1152])]video_metadatas [{fps:30, frame_indices:[0, 31, 62, …, 899], total_num_frames: 900}经过processor处理后的inputs结果为1234567{input_ids: tensor([[151644,872,198, ...,151644,77091,198]]),attention_mask: tensor([[1,1,1, ...,1,1,1]]),pixel_values_videos: tensor([[0.2000,0.2000,0.2000, ...,-0.1529,-0.2549,-0.3176],[0.1843,0.1843,0.1843, ...,0.1843,0.1137,0.0824],[0.1451,0.1451,0.1451, ...,-0.1373,-0.0667,-0.0275],...,[0.4431,0.4588,0.4745, ...,0.2078,0.2000,0.2000],[0.4510,0.4588,0.4588, ...,0.2157,0.2314,0.2471],[0.5451,0.5529,0.5686, ...,0.3725,0.3882,0.3882]]),video_grid_thw: tensor([[15,40,72]])}首先看video_grid_thw这个表示的patch布局前面提到缩放后的分辨率为640 x 1152对应到patch16后的patch布局则为40 x 72。但这里有个问题为什么采样时帧数为30这里变成了15通过阅读Qwen3VLVideoProcessor的源码[1]可以了解到有一个重要参数是temporal_patch_size2也可以在模型的config文件里看到这个参数。这个参数代表的含义是模型一次看多少帧作为一个token。也可以理解为每个video token覆盖的连续帧数。在这里指定为2也就表示每2帧合并成1个时间patch这样就天然包含了帧序列中运动/变化的信息。这个参数越小能捕捉到的动作变化就越细对应消耗的token也越多。所以这就解释了为什么输入模型的patch布局为15 * 40 * 72。再看pixel_value_videos的shape为torch.Size([43200, 1536])第一维的43200很好理解就是这个视频的patch数量也就是15 * 40 * 72 43,200。现在我们已知输入模型的1个vision token也就是patch token的组成为2帧 * patch像素。每个patch对应的像素为2 * 3(RGB通道) * (16*16) 1536这就对应到了pixel_value_videos的第二维也就是每个patch的维度。最后再看input_ids结果其shape为([1, 10938])对其做decode的结果为12345|im_start|user0.3seconds|vision_start||video_pad|*10800表示出现10800次|video_pad||vision_end|请描述这个视频的内容|im_end||im_start|assistant这里我们已知|video_pad|是作为后续video patch token的占位符。那为什么上面计算得到的原始vision patch的数量为43200但到真正转为token ids时就只有10800个vision token了这涉及到另一个参数为spatial_merge_sizeQwen3-VL-4B-Instruct里默认为2。这个参数是一个空间下采样/特征合并参数用来控制“把相邻的小 patch 聚合成一个大 token”的粒度从而直接决定视觉序列的长度与后续计算量。更直接点说就是为了减少后续attention的计算量从而节约计算资源。如果取值为2则表示把2 x 2 个原始patch特征再拼接成1个token从而减少4倍的序列长度所以merge后为10800个token id也就是43200 / 4 10800。最后在运行model.generate()时将|video_pad|替换为merge后的patch特征便构造了视频推理模型的输入开始执行prefilldecode推理流程。回到顶部2. 模型全参微调在理解了视频推理的流程后继续进行模型微调。首先需要准备一些视频素材并进行打标。2.1. 视频训练数据准备这里我们从开源数据集MicroLens[2]中选取了部分王者荣耀视频的数据并对其做了切分将视频切分为15s以下的片段然后人工补充描述。对于一个示例片段让Qwen3-VL-4B-Instruct进行描述123456789101112131415161718输入描述这个视频并进行打标,使用json输出标签输出为{video_description:视频展示了一款多人在线战斗竞技游戏的实时对战场景。画面中玩家操控角色在游戏地图上进行激烈战斗使用技能和装备进行攻击和防御。屏幕左下角显示了一位玩家正在直播他专注地操作着游戏背景是蓝色条纹窗帘。游戏界面显示了角色的血量、技能冷却、装备状态以及小地图等信息。战斗过程中角色不断释放技能造成伤害并获得金币。屏幕右下角有操作按钮包括技能释放和移动控制。画面中还出现了中文文字提示如“老夫子跟我们开团吧”和“他凭什么跟我打”显示了玩家在游戏中的对话和策略。整体氛围紧张刺激充满竞技性。,tags: [游戏直播,多人在线战斗竞技,角色扮演,技能战斗,游戏界面,直播主播,竞技对战,游戏策略,中文文字,实时战斗]}下面我们对这类游戏视频使用以下模式进行打标例如12345678910111213141516171819202122{video:wzry/4/1.mp4,conversations: [{from:human,value:video\n描述这个视频并进行打标,使用json输出标签},{from:gpt,value: {video_description:视频展示的王者荣耀游戏的界面。在画面中首先展示的是角色信息界面和他们的战绩角色包括凯、元歌、嬴政。然后又展示了角色澜、狄仁杰和不知火舞。接下来是男主播操作凯的记录画面左下角的小框是男主播的视频。整体画面中展示了凯和敌方交战的场景其中凯发动了变身技能和三个敌方单位进行交战。在交战过程中凯终结了狄仁杰。,tags: [游戏直播,王者荣耀,凯,狄仁杰,澜,战斗画面]}}]后续按照这个模式准备对应的训练数据。2.2. 模型训练首先我们使用小模型进行测试并验证效果使用Qwen3-VL-4B-Instruct模型参考官方提供的训练脚本进行训练[3]。训练模型的命令为123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172torchrun \# 分布式启动 --nproc_per_node8\--master_addr127.0.0.1\--master_port29500\# 训练入口 Qwen3-VL/qwen-vl-finetune/qwenvl/train/train_qwen.py \# DeepSpeed --deepspeedQwen3-VL/qwen-vl-finetune/scripts/zero3.json \# 模型与数据 --model_name_or_pathQwen/Qwen3-VL-4B-Instruct \--dataset_usewzry_video%100\# 输出与实验标识 --output_dir./output/wzry_video_finetune \--run_namewzry_video_qwen3vl_4b \# 多模态可训练模块 --tune_mm_visionTrue\--tune_mm_mlpTrue\--tune_mm_llmTrue\# 精度与训练轮数 --bf16 \--num_train_epochs20\# Batch 累积 --per_device_train_batch_size2\--per_device_eval_batch_size4\--gradient_accumulation_steps4\# 学习率分模块 --learning_rate2e-05\--mm_projector_lr4e-05\--vision_tower_lr2e-06\# 优化器与调度 --weight_decay0.01\--warmup_ratio0.03\--lr_scheduler_typecosine \--max_grad_norm1.0\# 上下文与数据组织 --model_max_length8192\--data_flattenTrue\--data_packingFalse\# 视频采样参数 --video_fps4\--video_max_frames32\--video_min_frames16\# 视频分辨率 / token 控制 --video_max_pixels1003520\--video_min_pixels401408\--max_pixels602112\--min_pixels100352\# 评估与保存 --eval_strategyno \--save_strategyepoch \--save_steps2\--save_total_limit3\# 日志 --logging_steps2\--report_totensorboard \--gradient_checkpointingTrue\--dataloader_num_workers4其中我们介绍几个在视频理解模型里的参数1. 多模态参数是否可训练包括Vision Encoder多模态投影层和语言模型a) --tune_mm_visionTrue --tune_mm_mlpTrue --tune_mm_llmTrueb) 也就是说这是个全参微调适合于GPU资源比较充足的情况达到更好的效果。否则可以冻结部份参数2. 视频参数a) --video_fps4 --video_max_frames32 --video_min_frames16b) --video_max_pixels1003520 --video_min_pixels401408 --max_pixels602112 --min_pixels100352c) 这几个参数是控制帧采样与单帧分辨率防止GPU OOM和token帧数太多的情况将token数resize到合理的空间范围内。和推理时指定的参数类似所以在gpu资源有限时除了常规的减少batch size、model_max_length、切换zero stage、使用lora等还可以调整上述的例如冻结Vision Encoder层、控制视频参数等来进行进一步的减少gpu的需求。从训练过程来看收敛良好下面测试全参微调后与原始模型的对比效果使用另一个示例视频可以看到原始模型对视频做了整体的理解可以正常理解为游戏画面以及视频的大致内容123456789101112131415python test_inference.py--model Qwen/Qwen3-VL-4B-Instruct--video ../data/wzry/57/2.mp4--prompt描述这个视频并进行打标,使用json输出标签使用中文。{video_description:这是一段《王者荣耀》的游戏录像展示了玩家在游戏中的战斗和操作过程。画面中可以看到多个英雄角色在河道区域进行激烈对战其中包括使用技能攻击敌方单位、躲避伤害以及利用地形优势进行战术配合。屏幕左上角显示了小地图和队伍状态右下角则有技能按钮和伤害数值提示。整个场景充满动感展现了快节奏的MOBA竞技风格。,tags: [王者荣耀,MOBA游戏,英雄对战,技能释放,团队协作,游戏直播,战斗场面,游戏策略]}然后再看微调后的模型效果可以看到视频对内容理解的更为精准1234567891011python test_inference.py--model ~/multi-model-train/output/wzry_video_finetune/--video ../data/wzry/57/2.mp4--prompt描述这个视频并进行打标,使用json输出标签{video_description:这是王者荣耀游戏界面刚开始是角色“百里守约”的游玩记录左上角显示的是敌方信息和我的方信息。在画面中“百里守约”开枪击败了角色“苏烈”。然后“百里守约”受到了敌方的攻击在使用技能位移了一段距离后发动技能与对方角色“澜”交战并将对方打成残血。,tags: [王者荣耀,百里守约,澜,战斗画面]}回到顶部3. 总结本文围绕 qwen3-vl-4b-instruct对多模态视频理解中的推理与全参数微调过程进行了完整实践从视频输入构建、推理执行到基于真实游戏视频的数据准备与训练配置系统梳理了多模态模型在视频场景下的工程细节。后续我们将进一步介绍如何对视频这类非结构化特征进行统一管理并逐步完善模型的持续训练 pipeline包括特征处理、版本化与管理机制以及模型版本的管理与部署以支撑更稳定、可扩展的多模态应用落地。