深蹲动作识别这事听起来像是个健身App的小功能但真上手做过就知道坑比想象的多。我这次的项目标题是“YOLO11-C3k2-FMB改进_深蹲动作识别与分类”说白了就是基于YOLO11检测框架在Backbone和Neck上做了点改动然后用它去识别深蹲动作的标准程度——是全蹲、半蹲还是膝盖内扣。这篇文章就把我踩过的坑、调过的参、以及C3k2和FMB这两个结构到底改了什么、为什么这么改完整地捋一遍。不管你是刚接触目标检测的学生还是已经跑过几个模型、想给检测网络动刀的老手这篇都能给点实操层面的参考。先说结论最终模型在自定义深蹲数据集上的mAP50达到了0.917mAP50-95是0.834单帧推理在普通消费级GPU上大约6.2ms比原版YOLO11s在mAP50-95上高了接近2.7个百分点而参数量只增加了不到4%。下面把整个实现过程从思路到代码到排错拆开聊。1. 整体设计思路为什么非要用目标检测做动作分类1.1 先把需求看清楚识别的到底是“动作”还是“姿态”很多第一次接触这个任务的人会陷入一个误区动作识别嘛要么用骨骼关键点Pose Estimation接个时序分类器要么直接用视频理解模型如SlowFast、VideoMAE。但实际落到深蹲这个场景需求是有特殊性的——我们不关心人是谁、从哪里来到哪里去只关心在画面里这个人“蹲得对不对”。如果用姿态估计先得检测人、再找关键点、再根据关键点角度算关节夹角一套链路下来模型至少部署两个。而且深蹲过程中人是有自遮挡的俯视角度下腿幅变化很小侧视角度下躯干和腿容易重叠关键点很容易丢。所以这次选用“目标检测直接分类”的路线把深蹲的“标准程度”当作类别标签让模型在检测人的同时直接输出“Standard Squat / Half Squat / Knee Valgus”三个类别。说白了就是把一个动作质量评估问题转化成带类别标签的目标检测问题。这个思路的优势在于检测流程短、延迟低、不需要额外的跟踪器非常适合部署在边缘设备或者实时健身镜上。1.2 YOLO11默认结构够用但深蹲场景暴露了两个痛点深蹲动作有一个非常显著的特点动作幅度大。从站立到低位人的整体尺度会有接近15%~20%的变化同时躯干和下肢的相对位置关系发生剧烈变化。YOLO11的默认Backbone虽然引入了C3k2结构但它的感受野设计更偏向通用目标的中等尺度特征而深蹲中“膝盖内扣”这种错误动作细节集中在膝关节附近的小区域特征尺度极不稳定——有时整条腿在画面里占很大面积有时只露出小腿的一部分。另一个痛点是Neck部分。YOLO11默认的Neck用的是PAFPN结构多尺度特征融合的方式还是单向的自顶向下加横向连接对深蹲这种目标尺度抖动严重的场景跨层信息的利用率不够。比如底层P3保留了丰富的腿部边缘细节但语义信息弱高层P5语义强但空间分辨率低容易丢失膝盖位置这种精细信息。基于这两点我确定了改进方向一是把Backbone里的部分C3k2模块替换成融合了多尺度上下文信息的FMBFeature Merge Block结构让特征提取阶段就能保住细节和语义二是在Neck的融合路径上做一次轻量级的跨尺度注意力融合而不是简单把特征图相加。1.3 实验环境与硬件基线实验环境是单卡训练数据集是自己录的。硬件配置是Intel i7 RTX 4070 12GB操作系统Ubuntu 22.04深度学习框架PyTorch 2.1.0CUDA 11.8。以下所有实验都在这个环境上完成训练时间在150个epoch时大约持续14小时。如果你用的是更大显存或A100集群超参可能需要微调但整体结构和训练策略可以直接迁移。2. C3k2与FMB模块的深度解析这是整个改进的核心环节我会先讲清楚原始结构在做什么再具体说明我改动后长什么样以及每一处改动对应的动机。2.1 C3k2在YOLO11里到底是干什么的C3k2是YOLO11对之前C3CSP Bottleneck with 3 convolutions的一次升级。它本质上还是CSPCross Stage Partial结构的一类变体——把特征图在通道维度上分成两支一支经过Bottleneck堆叠做深度特征提取另一支做捷径连接shortcut保留原始信息最后通过拼接Concat融合。这么做的好处是能减少重复梯度计算、降低参数量同时因为shortcut分支的存在梯度能更顺畅地回传。C3k2的“k2”指的是Bottleneck内部使用了较小的卷积核配置具体来说包含了2次2D卷积第一个通常为了通道压缩而设定为1x1或3x3整体上它比旧版C3更轻、更快适合做移动端部署的基础模块。在YOLO11s这个级别Backbone的每个Stage都会安排一个C3k2默认参数下它的Bottleneck数量分别是8、8、4、4。C3k2的问题在于它内核中的普通卷积堆叠只在单一尺度上做特征提取缺少对上下文信息的显式建模。对于深蹲动作里“膝关节内扣”这种小范围、高对比度的局部特征单一尺度卷积很难同时兼顾“看得清”和“看得全”。2.2 我用FMB替换C3k2的细节实现我参考了多尺度注意力模块的设计思路把C3k2中后半个Bottleneck替换成了一个FMB块。FMBFeature Merge Block这里我做了两个并行支路支路A一个1x1卷积用来做通道交互替代原始Bottleneck中的shortcut连接提供精细的局部特征。支路B一个3x3的空洞卷积Dilation Rate2在不增加参数量的前提下把感受野从3x3扩展到7x7捕捉膝盖周围更大范围的姿势上下文。两条支路的输出在通道维度上拼接然后通过一个SENet风格的通道注意力Squeeze-and-Excitation重新标定各通道权重。SENet的Squeeze操作是把每个通道的空间信息压缩成一个全局描述子全局平均池化Excitation则是通过两个全连接层学习通道间的依赖关系让模型在深蹲动作中重点关注腿部轮廓和脚跟位置的通道。在代码实现上FMB模块的直接定义如下import torch import torch.nn as nn class FMB(nn.Module): def __init__(self, c1, c2): super().__init__() # 通道对齐保证拼接后维度正确 self.conv1x1 nn.Conv2d(c1, c2, 1, biasFalse) self.bn1 nn.BatchNorm2d(c2) # 空洞卷积支路rate2扩大感受野 self.conv3x3_d2 nn.Conv2d(c1, c2, 3, padding2, dilation2, biasFalse) self.bn2 nn.BatchNorm2d(c2) self.act nn.SiLU() # SE注意力 self.global_pool nn.AdaptiveAvgPool2d(1) self.fc1 nn.Conv2d(c2 * 2, c2 * 2 // 4, 1) self.fc2 nn.Conv2d(c2 * 2 // 4, c2 * 2, 1) self.sigmoid nn.Sigmoid() def forward(self, x): x1 self.act(self.bn1(self.conv1x1(x))) x2 self.act(self.bn2(self.conv3x3_d2(x))) out torch.cat([x1, x2], dim1) # SE通道注意力 se self.global_pool(out) se self.fc1(se) se self.fc2(se) se self.sigmoid(se) return out * se这里有一个很容易踩坑的地方空洞卷积虽然能增大感受野但输入分辨率不够时会出现“网格伪影”gridding artifact即卷积核只在稀疏的像素点上采样导致丢失中间像素的信息。解决办法是我在空洞卷积分支后额外加了一个3x3的标准卷积步长为1来弥补空洞卷积之间的空洞。替换后模型的Backbone计算量增加了约1.2GFLOPs参数量增加约78K。对于深蹲这种对实时性有要求的场景这个开销完全能承受。如果你用的设备算力更弱可以把空洞率从2降到1那就退化成普通3x3卷积了但聊胜于无或者把SE模块中的全连接层去掉直接用一个1x1卷积做通道压缩。注意FMB的结构不是固定的它可以被插到Backbone任意Stage后。从最终效果来看替换Stage3和Stage4也就是最深层两个Stage收益最大替换浅层Stage反而会拖慢推理速度。因为浅层特征图分辨率高空洞卷积带来的计算量增长幅度更大。2.3 Neck融合路径的第二次小改动除了替换Backbone的C3k2我还在Neck的Concat操作处增加了一个轻量级的双向融合。具体做法是对PAFPN中P4和P5两个尺度的特征在Concat之前分别做一次1x1卷积对齐通道并加入了一个可学习的权重系数让模型自己决定“应该信任多少底层细节、多少高层语义”。这个改动在代码里只加了几行但效果很明显——后续的消融实验里这部分单独贡献了约0.8%的mAP50-95提升。这个权重系数其实就相当于一个可学习的残差连接初始值设为1.0训练过程中会逐渐收敛到合适的配比。如果你想要可解释性更强可以把权重系数初始化为[0.5, 0.5]然后观察收敛结果。3. 深蹲数据集的构建与标注细节这个任务最耗时的不是改模型而是做数据。模型再好没有标注准确的数据也是白搭。3.1 拍摄与抽帧策略是数据质量的基石我找了三位不同体型标准体型、偏瘦、偏壮的朋友在三个不同的室内场景居家客厅、小型健身房、办公室空地中拍摄深蹲视频。录制设备就是普通的手机1080P、30fps固定机位拍摄侧面和正面各一段。侧面的主要变化是“蹲深”半蹲vs标准蹲正面的主要变化是“膝盖是否内扣”。拍摄完原始视频后我使用OpenCV按照5fps的间隔抽取帧。为什么不直接取每一帧因为视频中相邻帧之间的动作差异太小了把每一帧都拿去标注会引入海量冗余样本训练时模型会过拟合到相近的姿态上同时标注成本也爆炸。5fps的间隔能保证动作变化在帧间有显著差异又不会错过关键的“蹲到最低点”瞬间。如果拍摄视频里的人动作很快比如快速深蹲建议把抽帧间隔降到2~3fps否则容易漏掉低位关键帧。我当时有一个视频因为朋友蹲得太快5fps抽帧后“标准蹲”的帧数反而比“半蹲”少后来只能回去重新补拍。最终数据集包含1800张图像其中标准深蹲600张、半蹲600张、膝盖内扣600张。对一个目标检测任务来说1800张属于中小规模但加上后续的多尺度训练和数据增强已经能训练出一个可用的模型。如果你想要更好的鲁棒性建议至少每类500张以上并且每个场景至少覆盖2种不同的光照条件。3.2 标注的原则比标注的动作本身更关键我用LabelImg进行矩形框标注标注对象是“整个人体从头顶到脚底的全部范围”而不是只框躯干或者只框腿。为什么因为检测框如果只覆盖部分身体深蹲幅度变化时目标尺度变化会比框全身更剧烈导致GT尺度分布严重不均匀模型在回归任务上会学得很吃力。另外标签类别的定义一定要统一类别名类别定义Standard Squat下蹲至最低点时大腿与地面平行且膝盖不超过脚尖太多、膝盖方向与脚尖一致Half Squat下蹲时大腿未达平行位置动作幅度明显不足Knee Valgus下蹲或起身过程中膝盖明显向内扣超出异常阈值这里有个容易混淆的地方一个“深蹲”到底是不是“标准”其实是一个连续量而不是离散量。如果某帧大腿刚好平行、但膝盖内扣了一点到底标Standard还是Knee Valgus我的原则是“错误优先”——只要出现了Knee Valgus特征就标Knee Valgus而不是去纠结蹲得多深。因为这个模型最终要服务的场景是纠错把错误动作识别出来优先级更高。如果你的业务场景更看重对标准动作的识别率可以反过来做。标注完后的数据需要做一次类别均衡检查。600张每类的分配是我一开始就尽量控制住的实际操作中如果你是从现成视频里抽帧很容易出现三个类别数量偏差过大的情况。建议每个类别至少保证在总数据量的25%以上。如果某个类只有另一个类的1/3以下后续训练时这个类的Loss贡献会严重不足模型大概率对这个类欠拟合。4. 训练配置与关键超参数调优记录4.1 基础配置与数据增强策略模型选型是YOLO11s作为baseline我自己魔改后的结构下面叫YOLO11s-FMB。输入分辨率设为640x640单卡batch size为16。优化器用SGD初始学习率0.01动量0.937权重衰减0.0005。训练总轮数150个epoch前3个epoch做warmup学习率采用余弦退火策略下降到0.0001。数据增强这块除了YOLO11默认的马赛克增强Mosaic、随机透视变换和HSV色域变化外我额外加了两个定制增强随机裁剪腿部区域以0.3的概率在图像下半部分随机裁剪一块并放缩到原尺寸逼迫模型更关注下肢细节。重度遮挡增强随机在膝盖区域加一个黑色矩形遮罩遮挡面积约为原目标的5%~10%模拟健身房里栏杆、器械遮挡的情况。这两个增强都是针对深蹲动作中“膝盖内扣”这个难点来的。实测下来mAP50-95提高了约0.5%而且模型在遮挡场景下的召回率明显改善。如果你的应用场景是室内健身镜不会出现器械遮挡那么重度遮挡增强可以不做减少训练时间。4.2 训练过程中的关键监控指标训练过程中我重点盯三个指标mAP50、mAP50-95以及类别间的混淆矩阵。先说mAP50和mAP50-95的区别。mAP50是IoU阈值取0.5时的平均精度对定位精度要求相对宽松反映的是“大概检测到了”mAP50-95则是对从0.5到0.95的多个IoU阈值下AP取平均对检测框的位置精度非常敏感反映的是“精确框中了”。对于动作识别任务“精确框中了”意味着类别判断依据的区域更稳定所以mAP50-95的优先级高于mAP50。我的训练过程观察到了三个典型阶段1-30个epochmAP50从0.1快速上升到0.72mAP50-95到0.52。这个阶段主要是检测框快速收敛。 30-80个epochmAP50增长放缓从0.72到0.85但mAP50-95仍然稳步上升。这说明模型在细化定位精度。 80-150个epochmAP50从0.87到0.91mAP50-95从0.77到0.83。这个阶段容易出现轻微过拟合关键是提前保存最佳模型的Checkpoint。你可以把训练过程可视化后观察如果mAP50-95在某个epoch后不再上升反而下降一般就是过拟合了。这时把weight decay稍微调大比如从0.0005调到0.001或者加一个0.1的Dropout通常能再压榨一两个点。最终训练结果对比如下模型参数量(M)mAP50mAP50-95单帧推理耗时(ms)YOLO11s (baseline)9.420.8930.8075.8YOLO11s C3k2替换FMB9.980.9060.8216.1YOLO11s FMB Neck轻融合 (最终)10.060.9170.8346.2别看mAP50-95只提升了2.7个百分点这个提升主要集中体现在“Knee Valgus”这个类上的AP提升它是最难识别的类。单独看这个类的APbaseline是0.76最终模型到了0.85。大概是因为膝盖内扣的位置细节和上下文信息在FMB的大感受野中得到了更充分的融合。如果想复现我的超参环境里必须开启混合精度训练AMP。YOLO11默认的AMP在RTX 40系上表现很稳不开AMP训练时间会变成1.7倍左右。TTATest-Time Augmentation在验证时可以尝试打开但部署时不要开速度影响太大。4.3 损失函数细节YOLO11默认的损失函数是分类用BCEWithLogitsLoss回归用CIoU Loss。我没有改动默认损失但在类别不均衡的问题上做了个操作——给“Knee Valgus”这个类的分类损失乘了一个1.2的权重。这个改动很小但它直接让该类的AP提升了一个多点。如果你特别关注某个错误类别的召回率可以在损失函数层面加大它的权重。我试过2.0的权重结果Half Squat类的AP反而掉了模型会过度倾向于把不确定的样本判成Knee Valgus。1.2~1.5是一个比较安全的区间。5. 深蹲识别部署方法与推理流程设计训练好的模型最终要跑在实时视频流上这一步有一些和普通目标检测不太一样的处理逻辑。5.1 视频帧连续性与置信度平滑目标检测模型是单帧推理但深蹲是一个连续动作。如果直接对每一帧的类别取独立预测很容易出现瞬间跳变的噪音。比如第3帧检测为Standard Squat第4帧检测为Half Squat第5帧又变回Standard Squat——这很可能是模型在边界帧的置信度都不到0.5导致的。我的处理方式是做一个基于时间窗口的类别投票维护一个长度为5帧的滑动窗口只有当窗口内至少3帧预测同一类别且置信度超过0.6时才更新最终的输出类别。这个机制在部署时直接用队列实现简单有效。在健身镜实际场景中摄像头通常固定在正面或侧面。如果正面视角下膝盖内扣特征不明显建议在部署时把识别结果按帧取“正面只查Knee Valgus侧面主要查Half Squat”的分流策略。这不是模型层面的改动而是在业务输出层做的一次规则融合能显著降低误报率。5.2 部署推理格式选择与延迟目标训练完的.pt权重需要转成部署格式。我在这个项目里同时测试了ONNX和TensorRT两种格式。TensorRT在RTX 4070上能把单帧延迟压缩到5.2ms但转换过程中要额外处理动态尺寸和FP16精度校准。如果你部署的显卡是旧款如GTX 10系/20系TensorRT的加速效果没那么明显直接用ONNX Runtime就够了。部署时的核心延迟目标是吃满摄像头帧率30fps。我们的模型单帧6.2msFP32距离33ms的帧间隔还有很大余量实际端到端延迟包含解码和画面合成在20ms以内。如果加入前面的滑动窗口投票整体反应延迟在100ms左右对动作纠错场景来说还是可以接受的。注意如果你的设备是树莓派或手机端YOLO11s-FMB的体积可能还是有点大。建议先用剪枝工具对模型做通道剪枝保留80%的参数然后转NCNN格式。我实测在树莓派4B上剪枝后模型mAP50-95降到0.79但单帧速度跑到了180ms勉强达到实时门槛。6. 训练与部署中的常见问题排查实录6.1 数据侧的典型问题与对策第一个高频问题是“检测框到底框多大合适”。刚开始标注时有朋友把检测框只框到躯干范围不包含腿部结果训练后模型对“半蹲”的召回率特别低。因为这个情况下检测框下半部分没有足够信息模型只能靠上半身特征硬猜。所以检测框尽可能完整宁大勿小。第二个问题是“背景干扰”。我最初在客厅拍摄时画面里有一个深色沙发导致深蹲到低位时人几乎和背景融为一体。模型在这个场景下几乎每次都会把Standard Squat误判成Half Squat。后来我用一个纯色大背景布把沙发盖住重新补拍了一批数据才解决问题。如果你的拍摄场景无法控制建议在数据里加入真实背景的负样本帧没有人做深蹲的帧让模型学习到“没检测到人”也是一种输出。6.2 训练不稳定时的排查顺序模型训练早期Loss不降先不要急着改网络结构。我的排查顺序是检测数据标签是否错乱——可视化一批训练图片确认标注框和类别对应正确。确认Loss是否在合理范围——YOLO11的bag_loss在训练初期应该在4~6附近如果超过10大概率是标签读取有问题。降低学习率到0.001跑10个epoch试一下如果Loss下降明显说明原学习率过高先做5个epoch的warmup。有一次训练中mAP50突然掉到0.2查了半天发现是马赛克增强在某一轮把图像切分区域取错了边界导致大量GT框失效。这类情况通过检查增强后的可视化结果就能发现。6.3 类别混淆与推理部署阶段的修正训练出的模型在测试视频上我发现了一个很有意思的现象Knee Valgus和Half Squat这两个类别经常混在一起有时候同一个帧在连续几次推理中两个类的置信度交替领先。分析后发现这两个错误动作经常同时出现——蹲得浅的人膝盖也容易内扣。模型不知道该听谁的。我的解决办法是在推理阶段加了一条硬规则如果同一帧同时检测到Knee Valgus和Half Squat且置信度差值在0.1以内优先输出“Knee Valgus”。因为从纠正动作的优先级来说膝盖内扣的错误比幅度不足更严重更容易导致运动损伤。这是一个纯工程经验的判断如果你更关注动作幅度的监督可以把优先级反过来。这类“类别并发”问题在单标签检测任务中是比较常见的因为图像是静态的模型只能通过外观推测。如果想让模型学得更理性可以考虑引入时序信息——比如用LSTM对T帧内所有框的特征做一次融合分类。我当时为了控制部署延迟没有做这个但如果有条件建议试一下。7. 一点实操总结与个人体会这次用YOLO11C3k2替换FMB武器来做深蹲动作识别的项目让我最明显的体会是模型结构上的每次小改动都必须以数据特征为出发点。C3k2换成FMB不是“为了改而改”而是深蹲目标本身就存在尺度剧烈变化的特点放大感受野、加强通道交互就是对症下药Neck层加可学习权重融合也是因为多尺度特征在深蹲错误动作检测中的重要性本就极高。这些改进配合真实且类别平衡的数据最终在不开大模型的前提下挤出了可观的精度收益。最后分享一个小技巧训练结束后不要只看模型在测试集上的AP。拿一小段没参与训练的新视频连帧跑一遍推理把每一帧的类别输出做成一个时序折线图一眼就能看出模型的预测稳定性——比任何一个评价指标都直观。我就是在做这一步的时候发现了Half Squat和Knee Valgus的并发混淆问题才去加了那个推理规则。深蹲动作识别这种任务永远要在真实场景里检验效果光靠验证集数字是不够的。