首页 / 资讯中心 / 文章详情

视频分类如何用VGG16微调?拆帧转图片实现91%准确率

视频分类如何用VGG16微调?拆帧转图片实现91%准确率 ★ FEATURED ARTICLE
最近我在整理手头一批短视频数据时遇到一个挺常见的需求视频分类数据集虽然信息丰富但标注成本高、训练起来也不方便。于是我想了个折中方案——把这些视频按一定规则拆成图片把视频分类问题转换成更成熟的图片分类问题然后在VGG16上跑一遍微调看看效果到底行不行。这个过程踩了不少坑也收获了一些超出预期的结果所以把完整思路和实操记录整理出来希望对做视频数据落地、或者想复用图像分类模型解决视频类问题的朋友有帮助。如果你现在手头也有一份视频分类数据集又不想一上来就上3D卷积或者视频Transformer那这篇文章就是给你准备的。我用的是一份包含12类动作的公开短视频数据集视频时长大多在2到6秒之间分辨率有720p也有1080p。整个流程走完验证集准确率做到了91%以上比起直接用原始视频分类管线训练时间缩短了一整个数量级。下面我把整个链路拆开讲从拆帧策略到数据清洗再到VGG16微调细节和最终效果每个环节都会说清楚“为什么这么做”。1. 一个视频任务为什么要绕道变成图片任务1.1 视频分类和图片分类的本质差别视频分类和图片分类看起来只差一个“时间轴”但实际差距非常大。图片分类模型核心要学的是空间特征比如纹理、颜色、形状结构视频分类模型除了这些还得建模动作在时间上的演变比如“挥手”是一组连续的姿态变化单看某一帧可能就是手抬起来而已。但时间信息不是所有任务都需要。相当一部分视频分类场景类别差异主要体现在关键帧上比如区分一段视频是在“打篮球”还是“踢足球”单帧画面往往已经能提供足够强的线索。这种情况下视频分类任务完全可以降维成图片分类任务用成熟的图像分类模型去处理反而更快更稳。1.2 拆帧带来的四个实际收益我这次把视频数据拆成图片后最直观的感受是四个字省心省力。具体来说有四个实际收益也解释了我为什么选择这条路。第一推理成本大幅下降。视频分类模型为了建模时序关系计算量会比图像模型高一个甚至两个数量级。拆帧后每个样本就是一张224x224的图单卡也能轻松跑批量推理。第二数据可视化变得容易。视频数据很难直接“看”但如果拆成图片你可以用文件夹形式直接浏览每一类长什么样、有没有标注错误、哪些视频质量差这对做数据质量分析非常友好。第三模型选择空间大。图片分类领域的预训练模型非常丰富VGG16、ResNet、MobileNet随便挑而且针对图像分类的调参经验、部署工具链都成熟得多不用自己造轮子。第四符合基线验证需求。有时候你并不知道一份视频数据的好坏直接训练视频模型一旦效果差很难判断是数据问题还是模型问题。转成图片分类后你可以先在图像模型上验证数据本身的判别性再决定要不要上时序模型。说白了拆帧就是一个“低成本试错”的手段。1.3 这条路的边界在哪里当然拆帧并不适合所有视频任务。如果类别本身的差异强烈依赖时序比如“翻跟头”、“跑步”和“走路”单帧姿态可能高度相似这时候图片分类模型的天花板就很低。我在项目里也遇到过这种情况部分类别即使把整个训练集都喂进去准确率也上不去就是因为信息确实不在单帧里。所以我的建议是先看你的数据类别是否能在单帧上区分。怎么判断随机抽几个类别的视频抽出一批帧来人工看一眼如果凭肉眼能从单帧分辨出类别就可以放心拆帧如果连人眼都分不清那就别硬转图片任务了。2. 拆帧与标签映射从视频目录到训练集的完整操作2.1 先看一眼手头的数据长什么样我把这份数据集称为“模拟项目X”它一共12个类别每类大约40到60段视频总计600多段。每个视频的文件名遵循一个基本约定类别名_编号.mp4比如basketball_012.mp4、drinking_007.mp4。这种命名方式对后续批量处理非常友好。数据集中视频的长短参差不齐短的只有1秒多长的能到10秒左右。解析这些视频的元信息我用的是ffprobe它能快速读出视频时长、帧率、分辨率方便后面统一规划抽样策略。2.2 目录结构与命名规范拆帧前先把目录结构定好这是一个极其重要但特别容易被忽略的细节。我见过不少第一次做这个操作的朋友把拆出来的帧随手丢到一个目录里最后做标签映射时痛苦不堪。正确做法是让目录结构自带标签信息省去单独维护映射关系的麻烦。我的目录设计如下dataset/ ├── train/ │ ├── basketball/ │ │ ├── basketball_012_frame_0001.jpg │ │ ├── basketball_012_frame_0002.jpg │ │ └── ... │ ├── drinking/ │ └── ... ├── val/ │ └── ... └── test/ └── ...每个视频拆出来的帧都放进以“类别名视频名”命名的子目录中帧文件名带上视频编号和帧序号。这样做有三个好处一是后续做训练集、验证集划分时可以按视频粒度的目录操作二是如果发现某个视频整体质量有问题可以直接删掉对应目录三是文件排序和可视化一目了然。2.3 用ffmpeg按帧率抽帧拆帧工具我用的ffmpeg没有第二个选项。它稳定、跨平台、命令行接口清晰还支持批量处理。单条抽帧命令如下ffmpeg -i basketball_012.mp4 -vf fps5 basketball_012_frame_%04d.jpg -q:v 2这条命令会把输入视频按每秒5帧的速率抽帧输出文件名依次带上4位序号。-q:v 2控制的是jpg压缩质量数值越小质量越高我一般设在2既保证图像清晰又不会让文件膨胀得太厉害。如果视频里有黑边或画幅不统一可以顺手加一个裁剪或缩放参数比如ffmpeg -i basketball_012.mp4 -vf fps5,scale224:224 basketball_012_frame_%04d.jpg -q:v 2但我不太建议在拆帧阶段就直接缩到224x224。原始分辨率保留得越完整后面做数据增强和预处理时越灵活。这一步我只做了均匀缩放处理统一到短边不小于256像素训练时再交给数据管线裁剪。2.4 帧率到底选多少帧率选择是整个拆帧流程里最需要动脑子的参数。帧率太高一个10秒的视频能拆出几百近千张图其中大量相邻帧几乎一模一样既浪费存储又加重训练负担帧率太低又会丢掉关键动作信息导致模型学不全。我先后试了1帧/秒和5帧/秒两种策略数据的规模差异非常明显。1帧/秒的方案总共拆出8000多张图5帧/秒能拆到38000多张。最终效果我在后面第5章详细说明简单一句话就是对于这个数据集5帧/秒的收益在分类准确率上是实打实的从87%左右提到了91%以上并没有因为增加冗余帧而拖垮训练。如果你不确定选多少我建议先按3到5帧/秒试一轮。这个区间能保留动作连贯性又不会让数据量爆炸。拆帧后顺手看一遍每类的图片数量如果某个类别的图片数明显偏多或偏少再决定是否调整。3. 数据质量把关清理掉拖后腿的帧才能喂给VGG163.1 连续帧高度相似先做一次去重拆完帧后第一时间需要做的不是训练而是看看拆出来的图片到底长什么样。我当时用文件管理器随机翻了一遍发现两个明显问题。第一个问题就是相邻帧相似度极高。5帧/秒的抽帧频率下一个镜头内连续两帧的像素差异可能只有几KB的大小人眼看着几乎一样。这种重复样本会让模型在训练时不断强化同一张图的特征对泛化能力帮助有限还会拖慢训练速度。我做了个简单的去重对每个视频内部逐帧计算与前一帧的灰度直方图相似度如果相似度超过95%就跳过该帧。也可以用像素差均值来判断阈值设成小于5就认为是重复帧。实际操作中这一步能去掉大约三到四成的冗余帧数据量从38000多张压缩到25000张左右而且训练效果不降反升。一个常见规律是少而精的数据往往比海量重复数据更容易训出稳定模型。3.2 脏帧过滤黑边、花屏、字幕干扰第二个问题是脏帧混入数据。视频数据里经常出现黑场过渡、片头片尾字幕、甚至少量花屏帧。这些图像和真正的动作内容无关把它们放进训练集等于在给模型添加噪声。过滤脏帧我用的是最简单可行的方法先把所有帧缩略成小图用亮度均值和方差做粗筛。均值和方差都异常的帧比如整体接近纯黑或纯白直接剔除。对于字幕干扰如果字幕只出现在固定区域可以做一次区域裁剪裁掉底部大约10%的区域再训练。我在这个项目里发现模拟项目X有好几段视频带水印但水印位置固定且都在左上角所以处理起来不算难——固定裁剪掉左上角一块区域即可。这种“按区域裁剪”的方式虽然丢了一点画面信息但换来的是模型不被水印文字干扰整体收益是正的。如果你处理后发现模型还是被某些杂讯影响比如突然出现跳帧撕裂可以考虑把对应视频段整段删掉宁可数据少一点也要保干净。3.3 类别不平衡的处理视频数据集天然容易类别不平衡因为每段视频时长不同同一个类别不同视频的画面信息量也不同。我统计了一下拆帧数量发现最少的类别只有800多张最多的类别有3000多张差距接近4倍。直接拿这种数据去训VGG16模型大概率会对样本多的类别产生偏好。我的处理方案分两步先按“每个视频最多保留N张帧”做一次上采样约束比如每个视频最多保留150张超出部分随机丢弃再对整体数据使用加权采样WeightedRandomSampler让每个类别在每次epoch中被采样的概率尽量均衡。实际效果是最少的类别参与训练的次数明显提升整体准确率也稳了一些。3.4 数据划分防止泄漏这一节我要重点强调一个最容易翻车的问题训练集和验证集的划分必须按“视频”划分而不是按“帧”划分。很多人图省事把所有帧混在一起然后随机打乱按比例划训练集和验证集。这个做法隐患很大。同一个视频拆出的帧之间有高度相似性如果一部分帧进了训练集另一部分帧进了验证集那验证集的数据和训练集“撞车”了模型看到的其实已经是见过的内容验证准确率会出现虚高。正确的做法是先给每个视频分配一个全局唯一的ID然后按照视频ID去划分数据集。比如我用的是一个简单的脚本先把所有视频ID列表随机打乱取80%作为训练视频20%作为验证视频再按视频归属把所有帧分到对应的集合。这样能保证同一个视频的所有帧始终待在同一个集合里验证结果才有说服力。4. VGG16微调模型改造、训练配置与参数调试过程4.1 为什么这次选择VGG16选模型这件事我本来可以用ResNet或者更轻量的EfficientNet但这次实验我用的是VGG16原因有三条。一是承接已有基线。这个项目的早期版本已经用VGG16跑过一轮为了保证和之前结果的对比性我继续沿用VGG16而不换架构这样效果差异可以归因到数据而非模型改动。二是VGG16结构非常直白。它由13个卷积层和3个全连接层组成卷积层分成5个block每个block后面接一个最大池化。这种层级规则的结构适合用来做特征可视化也方便我调试中间层。对教学和实验性质的场景来说VGG16是一个很好用的“透明箱子”。三是VGG16预训练权重在ImageNet上表现稳定迁移到小规模视频拆帧数据上不容易翻车。它参数量确实多大约1.38亿在单卡上训练略慢但还不至于跑不动。如果你的数据规模很小几千张VGG16反而比超大模型更不容易出现过拟合。4.2 模型改造与权重加载VGG16的默认输出层是1000类显然不适配我们的12类任务。我用的是PyTorch标准的改造方式把分类器最后一层替换成12个神经元的全连接层。import torchvision.models as models import torch.nn as nn model models.vgg16(weightsmodels.VGG16_Weights.IMAGENET1K_V1) num_features model.classifier[6].in_features model.classifier[6] nn.Linear(num_features, 12)这里有个关键决策到底冻结前面的卷积层还是全部微调如果数据量很少冻结全部卷积层只训练最后的分类器是最保险的如果数据量有上万张可以从某个中间层开始放开微调。我这次做了实验对比。方案A是冻结前4个block只微调最后一个block和全连接层方案B是全参数微调。方案B在验证集上高出了约3个百分点但训练时间也长了不少。最终我采用了方案B因为25000张图对VGG16来说已经可以让高层的卷积核做有意义的适应性调整了而且我加入了较强的数据增强来控制过拟合风险。4.3 训练配置和超参数训练配置里我认为最重要的不是模型结构而是数据预处理和优化器设置。我先说数据预处理。VGG16在ImageNet上预训练时输入图像会经过固定的归一化均值和标准差是官方给出的0.485, 0.456, 0.406和0.229, 0.224, 0.225。这一点一定不能省略否则图像分布不对预训练权重基本发挥不出作用。另外我在训练时加入了随机水平翻转、随机裁剪、随机旋转等增强保证模型看到更多样的数据。优化器我选了AdamW初始学习率设成1e-3权重衰减1e-4。batch size是32在单张GPU上跑VGG16正好能塞下。学习率策略用的是余弦退火总训练轮数15轮加了早停机制如果验证集loss连续3轮不下降就停止。这里特别说一句AdamW里的权重衰减不要省VGG16参数量大不加正则很容易在几轮训练后开始过拟合。4.4 训练过程的观察训练开始后我观察到的第一个现象是前两轮loss下降极快从2.4左右迅速掉到1.1。这很好理解预训练权重已经具备很强的底层特征提取能力分类头面对的其实是一个接近线性可分的高质量特征空间所以前期收敛快。到第6轮左右验证集准确率开始进入平台期在0.85到0.88之间震荡。我当时怀疑是学习率偏大导致难以继续收敛于是把学习率降到3e-4并启用了余弦退火中的warm restarts。这个调整立竿见影验证准确率从第8轮开始重新爬升最终在第13轮稳定在0.91以上没有再出现明显的振荡。这里也暴露了一个VGG16的老问题最后一个池化层输出到全连接层的过渡非常快特征维度直接从7x7x512展平成25088维再接4096维的全连接层参数量巨大。所以全连接层的dropout一定要保留PyTorch预训练模型默认在两层之间设置了0.5的dropout我没有去掉它这是防止过拟合的重要防线。5. 分类效果实测准确率、混淆矩阵与误判较多的几对类别5.1 三个对照组的结果我做了三组对比实验用来回答“拆帧数据在VGG16上到底效果如何”这个问题。第一组用1帧/秒抽帧得到约8000张图第二组用5帧/秒抽帧并做了去重清洗得到约25000张图第三组同样用5帧/秒数据但移除预训练权重从头随机初始化训练VGG16。三组都用相同的训练轮数和数据划分方式结果如下表实验组数据规模是否使用预训练权重验证准确率1帧/秒拆帧8000张是87.2%5帧/秒拆帧去重25000张是91.4%5帧/秒拆帧去重25000张否78.6%差异非常明显。预训练权重的价值不用多说直接从78.6%拉到91.4%这就是迁移学习的威力。而帧率从1提高到5并补齐去重清洗后准确率提升了4.2个百分点说明更密集的抽样确实保留了一部分有益信息特别是动作发生瞬间的姿态变化。5.2 谁分得好谁总被搞混我打印了混淆矩阵发现12个类别里判别性强的类别——比如篮球、跑步、翻书——准确率都在95%以上。这些动作的画面结构差异大单帧特征就很鲜明。表现最差的是“喝水”和“倒水”这对组合两者互相混淆的比例接近两成。我从数据里抽了误判样本来看发现原因很直观两段视频在某一帧上都出现了“手持杯子近嘴部”的画面特征几乎一致模型根本没法从单帧上区分“喝”和“倒”。另一个常见误判是“走路”和“慢跑”在单帧上姿态差异很小尤其当画面里的人物比较小或者背景杂乱时模型基本靠猜。5.3 从误判反推数据问题这一步我觉得是整个实验最值钱的地方。单独看准确率并不能告诉你数据哪有问题但是把误判样本翻出来一看洞察马上就出来了。比如“喝水”和“倒水”的误判本质上说明这个类别的区分信息存在于时间轴单帧图像任务的上限就到这了。面对这类情况两条路可选一是接受现状在实用场景里搭配一个简单的时间段判定规则比如连续若干帧都识别为“喝水”才最终判定为喝水二是不要硬扛承认单帧分类的边界后续接入短时序模型或光流特征。我自己最后选择了在推理阶段做“多帧投票平滑”取出预测置信度最高的若干帧按类别投票把置信度低且摇摆不定的帧过滤掉。这个简单策略把最终推断准确率又往上抬了约2个百分点。另外一个发现是数据集中部分视频的标注并不干净。比如某段“打篮球”的视频其实包含较长的高空远景人物只占画面的一小块VGG16根本提取不到有效姿态特征。这种数据与其说是标注错误不如说是“标注粒度不够细分”更合理的做法是在拆帧前先把这类无效片段截掉而不是让模型硬学。6. 复盘与后续改进思路这次实验教会我的三件事6.1 第一个教训不要被虚高的准确率骗了我一开始犯过一个错在还没做按视频划分数据时直接把帧级随机划分跑了一版验证准确率冲到94%。当时我还挺兴奋直到后来改成按视频ID划分准确率掉到91%我才意识到前面那3个点全是数据泄漏带来的假象。这件事给我留下的教训是视频数据做图像分类时一切评估都必须以视频为独立样本单位。如果你只想快速看个大概帧级划分勉强能用只要结果要用于决策或发文就必须按原始视频划分否则你的数字不干净。6.2 第二个教训数据清洗的时间花得值拆帧、去重、过滤脏帧、类别均衡这一套流程花了我大约一天半的时间比训练模型的时间还长。但正是这些琐碎工作把验证准确率从最初的80%左右一步步推到了91%以上。很多初学者拿到公开数据集后第一件事就是开训这是最容易走弯路的地方。视频数据转图片数据源头数据的脏乱程度远超普通图片数据集。花时间把质量关把住了模型效果会自然而然提升而且提升幅度远大于换一个更大的模型。6.3 后续还能怎么改进如果你也想做类似的事情或者想在我这个流程上继续往前走我建议从三个方向入手。第一换更轻量的骨干网络。VGG16虽然好用但参数量和推理速度都不是最优解。同样的数据放到ResNet18或者MobileNetV3上训练时间能缩短一半以上准确率大概率不会差太多。对实际部署来说轻量模型带来的收益非常可观。第二引入短时序信息。如果时间来得及可以基于拆帧后的图片序列做“短窗口分类”比如把连续3到5帧的图片特征做拼接或投票再用一个轻量的分类头做融合。这样既能保留拆帧带来的数据红利又能部分找回时间维度信息模型的局部场景适应能力会强很多。第三用在线抽帧替代离线拆帧。离线拆帧会消耗大量磁盘空间而且冗余帧清洗逻辑固定后不易改变。改成在线抽帧——也就是训练时实时从视频里随机选帧并做增强数据规模只受视频数量约束不用预先存下来。这样数据轨迹更灵活模型每次看到的帧也更随机泛化能力理论上更好。不过在线抽帧对CPU读取和视频解码速度有要求工程复杂度会上升一截。最后再说一个我保留到现在的小习惯所有拆帧脚本、清洗脚本和数据统计结果都按日期归档目录名里写清楚数据处理版本。你会发现做实验时最耗时的不是跑模型而是回溯“当初这张图是用什么规则生成的”。版本控制这套规则不只在代码里用数据处理流程一样适用。
阅读完成 · 觉得有帮助?
咨询建站