☰
层级分类实战:B-CNN分支网络如何解耦粗粒度与细粒度识别
2026/10/14 23:02:12 网站建设 项目流程

我去年在某个细粒度图像分类项目里被一个问题卡了很久:数据集本身带清晰的层级标签,先是大的生活场景分类,再往下是物体具体种类,但用普通多分类网络去训练,效果总是不尽如人意。后来想明白了一个关键点——层级结构里的粗粒度信息和细粒度信息应该分开处理,而不是逼着一个平坦分类网络同时兼顾两者。于是我把方案切到B-CNN(Branch CNN)上,用分支结构去承接不同粒度的分类任务,粗粒度和细粒度的准确率才同时上来。

这篇是"层级分类"系列的续篇,默认你已经了解层级分类的基本定义:类别不是平铺在同一条线上,而是从粗到细形成树状结构,比如"动物 -> 鸟 -> 麻雀"。本文重点讲怎么用B-CNN把这种层级关系真正落到网络结构里——包括分支分叉点怎么选、损失函数怎么配、推理时怎么把粗细两个分支的结果融合起来,以及我实际训练中踩过的几个坑。适合已经能训通普通CNN、想进一步做层级分类或细粒度识别的朋友参考。

1. 为什么B-CNN适合层级分类:从全局分类到分支分类的转变

1.1 普通CNN在层级标签上的三个缺陷

先回顾一下我为什么放弃"所有类别丢进一个Softmax"的做法。表面上看,类别树最底层有N个叶子节点,那网络输出N类就可以覆盖任务需求,粗粒度类别完全可以靠叶子类别的自然分组推导。但实际训练时会遇到三个问题。

第一是特征挤压。叶子类别越多,共享分类层要拟合的判别边界就越细。粗粒度的共性特征(比如"猫科动物都有圆脸和竖瞳")和细粒度的差异特征(比如"不同猫品种之间耳朵间距不同")被塞进同一个特征向量,网络只能被动地选择保留对最终Top-1损失最有利的信息,粗粒度的语义往往被削弱。

第二是梯度主导。训练数据通常是细粒度标签不均衡的。某一组粗类别下的细类样本特别多,网络就容易把所有特征都推向区分这一大类的内部差异,而忽略了其他粗类之间的边界。结果验证集上粗类准确率忽高忽低。

第三是推理缺乏约束。平坦分类网络预测结果不会自觉遵守层级规则,比如同一个父类下的两个子类概率之和本来应该等于父类概率,但全局Softmax根本不管这个关系。我甚至遇到过网络输出"狗"的置信度高达0.9、但同时又输出一个和狗完全不同父类的第二高概率类别,这在业务上是没法解释的。

1.2 B-CNN的直觉:让特征在每个粒度上各尽其职

B-CNN的做法很直观:主干网络先把通用特征提取出来,然后在某个特征层分出两个分支——一个分支负责预测粗粒度类别,另一个分支在这个基础上继续做深层次的特征提取,负责预测细粒度叶子类别。两个分支共享主干大部分参数,但各自有独立的分类头和独立梯度。这样做的好处是,粗粒度分支不用和细粒度分支抢同一个全连接层,它可以专注于"这个物体属于哪一大类"的判别;细粒度分支也不用被迫在浅层就做出过于精细的区分,它可以从主干已经比较稳定的特征继续往下钻。

而且B-CNN天然契合层级推理的逻辑。粗分支的输出是一个概率分布,细分支的输出是另一个概率分布,两者相乘后可以得到"真实叶子类别"的联合概率:叶子类别的最终得分 = 它所属父类在粗分支上的概率 × 该叶子在细分支上的条件概率。这个乘法操作在测试时非常简单,但带来的约束效果却很明显。

2. B-CNN网络结构设计:主干、分支与级联推理的具体实现

2.1 网络拓扑:从哪里分叉、分叉后怎么走

我最终采用的B-CNN结构基于ResNet-50,但这里有一个很关键的设计决策:分叉点放在哪里,不能拍脑袋。

我一开始把分叉点放在ResNet第一个Stage的输出位置,也就是刚做完底层边缘纹理提取的地方。结果粗粒度分支倒也能学,但细粒度分支因为缺少整个主干的特征逐步抽象过程,收敛很慢。后来我把分叉点放到第四个Stage(res4)输出之后,效果明显改善。原因也很简单:粗粒度分类通常靠中级语义特征就能判断,比如整体轮廓、颜色分布、大体形状,这些信息在res4阶段已经非常丰富;而细粒度分类需要更高阶的部件级特征,例如鸟类的喙部、翅膀纹路、脚爪形状,这需要额外的res5阶段继续抽象。

具体来说,我的网络拓扑是这样的:

  • 主干部分复用ResNet-50的前四组残差块,输出res4特征图,尺寸为批次×1024×7×7(输入224×224时)。
  • 粗粒度分支:对res4特征做全局平均池化,接一个256维的全连接层,最后接一个维度等于粗类数K的输出层。
  • 细粒度分支:从res4特征继续接原ResNet-50的第五个Stage(res5),得到2048×7×7特征图,再做全局平均池化、接一个512维全连接层,最后输出维度等于叶子类别数N。

这里要注意,res4特征图同时被馈送到粗分支和细分支,两个分支不会互相遮挡梯度。反向传播时,粗分支的梯度只更新它自己的全连接层和主干中res4及之前的部分;细分支的梯度除了更新自己的分支外,也会更新主干。但二者在减法上没有直接竞争——唯一的竞争发生在主干参数的叠加更新方向上,这一点我后面会在训练节奏部分详细讲。

2.2 推理逻辑:先粗后细与条件概率的自然融合

训练完两个分支后,有两个推理方案可用。

方案一是级联预测:输入一张图,先看粗分支输出,取概率最大的粗类,然后只看该粗类下包含的所有细类在细分支中的概率,取其中最大值。这种方案的优点是简单,但坏处是如果粗分支自己就判断错了,细分支永远没有机会纠正。比如粗分支把一只猫判断成狗,那么就算细分支对"猫"下的某个具体品种概率很高,也不会被选中。

方案二是概率乘积融合:对每个叶子类别c,记其父类为p(c),计算最终分数 score(c) = P_coarse(p(c)) × P_fine(c)。其中P_coarse是粗分支的Softmax输出,P_fine是细分支的Softmax输出。最终预测取所有叶子类别中score最大的那个。这个方案不仅保留了细分支纠正粗分支错误的能力,还天然满足层级约束——某个叶子类别的最终分数不可能超过它父类被预测的概率,这样就消除了"父类概率低但子类概率高"的荒谬情况。

我自己在实验里也验证过:方案二比方案一的Top-1叶子准确率高大约2到3个百分点。所以在实际部署时,我都会建议用概率乘积而不是简单的夹逼式推理。计算量只多了一个Softmax和一次逐元素相乘,几乎可以忽略。

3. 损失函数的设计:粗分支和细分支为什么必须被同时监督

3.1 损失函数组合与λ参数调控

B-CNN的损失函数表面上看只是两个交叉熵相加:

L = L_fine + λ × L_coarse

但λ怎么取非常影响最终效果。我试过λ=0.1、λ=1.0和λ=5.0,发现λ太大时网络会过度优化粗分类,细分支的特征学习受到压制;λ太小粗分支又起不到正则化作用。

最终我选择λ=1.0作为初始值,然后针对不同需求微调。如果业务更看重粗分类准确率,λ可以设为2.0;如果更看重细分类准确率,λ可以降到0.5。另外我实际训练时还会对两个损失做加权调整,比如当某个粗类的样本特别多时,我会对该粗类的粗分支损失项做类别权重缩放。

更关键的一点是:两个分支的损失不是一起反传就完了。粗分支对应的梯度只更新主干和粗分支;细分支的梯度会更新主干、res5和细分支。我测试过让粗分支只做前向不参与主干梯度更新,也就是把粗分支从梯度流中截断,结果细分支Top-1准确率反而下降了。这说明粗分支的监督信息对主干特征学习是有帮助的,它提供了一种额外的语义正则化,让主干特征不仅在细粒度层面可区分,在粗粒度层面也保持结构。

3.2 条件Softmax与全局Softmax的差异:一个关键实验

训练细分支网络时,我一开始用的是全局Softmax:对N个叶子类别直接算交叉熵。直观上这没问题,但结合层级分类语义后我发现它有个隐含的缺陷:全局Softmax会让不同父类下的叶子类别互相竞争。比如在父类"鸟"下,细分支会努力区分麻雀和燕子;但"鸟"和"哺乳动物"下的某个类别在同一个Softmax里也会互相竞争,导致细分支参数的一部分精力花在区分跨父类的"粗语义"上,而这些能力粗分支已经具备了。

于是我改用了条件Softmax,也叫层级Softmax的做法:对每个叶子类别c,只在它同一父类下的所有兄弟类别中做归一化:

P_fine(c) = exp(z_c) / Σ_{j ∈ siblings(c)} exp(z_j)

这样细分支的输出被限制在每个父类内部,网络只需要学习"我已经知道这是鸟,它到底是麻雀还是燕子"。

实验结果非常直观:使用全局Softmax时,细分支容易在所有叶子类别上做均匀分布,Top-1细分类准确率大约86%;改成条件Softmax后,同一个分支结构下准确率提升到89%以上,而且训练曲线震荡明显减少。原因就是条件Softmax把"跨父类比较"的负担完全拿掉了,细分支的所有参数都被用来深化父类内部的判别。

不过要注意,条件Softmax有一个前提:父类标签在训练时需要一起提供,而且父类内部必须至少有两个叶子类别。对于只有一个子类的父类,它的细分支概率恒为1,等于没有监督,这种情况建议直接把该父类合并到它的上一级,避免浪费参数。

4. 层级标签组织与样本不均衡处理

4.1 标签表的组织方式与批次采样

B-CNN不像普通分类网络那样只需要一个label.txt,它至少需要两个标签列:一列是叶子类别ID,一列是父类ID。我的做法是用一个CSV文件组织,字段包括image_path、coarse_id、fine_id。fine_id是全数据集的叶子类别全局编号,coarse_id对应该叶子所在父类的编号。

训练时要注意一个很容易忽略的点:PyTorch的DataLoader每次随机采样一个批次,如果批次内某个父类下的叶子类别样本特别多,就会导致这个批次里粗分支的监督信号向某个父类偏斜。我在训练初期没管这些问题,结果粗分支在单个Epoch内准确率波动很大。后来我改成了按父类均衡采样的策略:每个批次先从父类集合里均匀采样K个父类,再从每个父类里均匀采样若干个样本。这样能保证每个批次内粗类数量相对均衡,训练更稳定。

4.2 粗类不均衡与细类样本过少怎么处理

层级分类的数据集往往天然不均衡。比如"动物"这个父类下可能有100个叶子类别,而"植物"下只有5个。如果直接训练,细分支在"植物"内部几乎没有判别压力。

对于粗类不均衡,我的做法是在粗分支的交叉熵损失上加权,权重取该类样本数占总样本数比例的倒数。这比简单的上采样下采样要好,因为它不会造成某个父类重复样本过多、过拟合。

对于细类样本过少,我走了另一条路:细分支先不直接学叶子分类,而是在预设一个中层粗类分支的情况下,把每个细类样本做混合增强。具体来说,我对同一父类下的不同叶子类别进行Mixup增强,比如把一张麻雀和一张燕子的图像按0.7与0.3比例混合,让细分支学习更细的局部纹理差异,而不是简单记忆整体外观。这个技巧在鸟类模拟数据集上,把样本数少于50的长尾细类准确率提升了接近5个百分点。

5. 训练过程中的关键时刻与踩坑记录

5.1 从分叉点冻结到渐进解锁:主干与分支的配合节奏

这是我踩过最大、也最隐性的一个坑:主干参数同时接收粗分支和细分支的梯度,如果一开始就用大学习率让两个分支一起冲击主干,主干很容易发生"灾难性遗忘"式的震荡。表现就是训练前几个Epoch损失下降很快,然后突然回升,验证集准确率始终上不去。

后来我调整了训练节奏,分三个阶段走:

  • 第一阶段:冻结主干,只训练两个分支的全连接头。这时主干用ImageNet预训练权重,两个分支需要从随机初始化开始学,学习率可以设为0.01,做5到8个Epoch,让分支先适应主干特征的分布。
  • 第二阶段:解锁主干后两个Stage(res4和res5),学习率降到0.001,主干和分支一起训练,但细分支的梯度回传比例稍微下降,我通过给细分支损失乘0.8来做到。
  • 第三阶段:全网络解锁,学习率降到0.0001,继续训练直到收敛。

这种渐进式的解冻方式避免了主干刚被唤醒时两个分支的梯度在方向上反复拉锯。我在对比实验中验证过:如果一开始就全网络解锁,最终细分类精度大约只有渐进方案的三分之二。

5.2 验证损失震荡、过度自信与不收敛的排查

细分支在条件Softmax时特别容易出现"过度自信"的情况:训练集上损失很低,但验证集准确率一般。原因是父类内部往往有一些极其相似、连人类都很难分的叶子类别,模型学会了用捷径特征去区分,但对遮挡、模糊样本毫无泛化能力。

我排查后做了三件事:

  • 在细分支的全连接层之前加入Dropout,概率设在0.3。这个操作对细分支有效,但粗分支不要加,因为粗分支本身特征就够粗,加了Dropout反而损伤语义。
  • 验证集上同时观察粗分支和细分支的混淆矩阵。如果粗分支的混淆主要发生在相邻粗类之间(比如"猫科"和"犬科"),说明主干特征在粗粒度上还不够清晰,我应该提升λ;如果细分支混淆集中于同一父类,说明父类内部特征还需要加强,这时应该加深细分支而不是增加主干容量。
  • 学习率调度用了余弦退火,并在最后10个Epoch做了EMA平均。这个细节让验证集细分类准确率稳定提升了约1.2个百分点。

5.3 推理时的常见错误:为什么会出现"粗分支说鸟、细分支说猫"

这个现象是很多第一次用B-CNN的朋友最容易慌的地方。我用概率乘积方案后依然遇到过一次奇怪结果:一张明明是鸟的测试图,粗分支输出"鸟"的概率是0.72,但细分支输出最高概率的却是"猫"下的某个品种。算乘积后,"鸟"下的叶子类别依然能排前面吗?能,但有些猫品种的细分支置信度极高,比如0.95,乘上0.28的粗分支概率,也可能压过鸟的子类。

问题的根子在于:细分支的条件Softmax只在同一父类内归一化,不同父类之间的细分支输出根本没有互相约束。所以在做概率乘积融合时,我额外对细分支输出做了一个全局校准:在验证集上统计每个叶子类别的平均细分支概率,然后用这个值去校准预测分数。这就相当于给每个叶子类别除以它先验出现的频率,消除某些父类内部本身容易被高置信度预测的系统性偏差。

6. 实测对比:B-CNN、普通CNN与多任务CNN在模拟数据集上的表现

6.1 实验配置与数据集

为了说明B-CNN在层级分类任务中的真实收益,我在一个自建的模拟数据集X上做了对比。数据集包含48个粗类、312个叶子类别,总样本约12万张。每个粗类下的叶子数量并不均匀,最少只有4个,最多有23个,样本分布整体呈长尾。

对照组有三个:

  • 普通CNN:ResNet-50直接输出312类,全局Softmax。
  • 多任务CNN:共享主干但两个完全独立的分类头(一个头输出48类,一个头输出312类,各损失相加)。
  • B-CNN:本文方案,条件Softmax + 概率乘积推理。

所有模型使用相同的数据划分、图像分辨率224×224、预训练权重和总共60个Epoch的训练预算。评估指标包括粗类Top-1准确率、叶子类Top-1准确率,以及层级匹配准确率(预测的叶子类别和真实叶子类别属于同一个粗类视作正确)。

6.2 结果与解读

结果如下表所示。

模型粗类Top-1准确率叶子类Top-1准确率层级匹配准确率
普通CNN91.2%81.6%89.5%
多任务CNN94.0%83.1%91.0%
B-CNN95.6%88.7%94.2%

普通CNN表现最差,这在预期之内;多任务CNN粗类准确率比普通CNN高不少,但叶子类准确率提升有限。原因在于多任务CNN的两个分类头共享的还是同一层特征,粗分类头和细分类头对特征的需求方向不同,形成了一定程度的特征拉扯。

B-CNN在叶子类准确率上比多任务CNN高出了5.6个百分点,这个差距主要来自两方面:一是细分支通过res5获得了更深层特征,二是条件Softmax消除了跨父类竞争。层级匹配准确率也最高,说明即使叶子预测错了,B-CNN也倾向于把错误控制在同一父类内部,这一点对业务场景非常重要——系统可以在提示"识别不准"的同时,至少给一个正确的大类范围。

6.3 我个人还会继续钻研的改进方向

B-CNN在模拟数据集X上的表现让我对层级分类产生了更多信心,但它不是终点。我接下来的探索方向有两个:

一是让分叉点可以从数据中学习。手工指定res4之后作为分叉点虽然不是大问题,但不同数据集的最优分叉位置可能不同。我打算参考可微架构搜索的思路,让网络自己决定从哪一层开始分叉,这样可以更自动地适应不同数据集的细粒度差异。

二是利用粗细分支之间的关系做更轻量的知识蒸馏。我从实验中观察到,粗分支的预测分布其实携带了"这个物体在粗粒度语义上接近哪些类别"的信息,这些信息可以用来约束同一个粗类下的细分类特征,让细分类特征在继承粗分支语义的同时更加紧凑。目前我在一个小规模植物数据集上做尝试,早期结果还算不错。

如果后面有新进展,我会在博客里继续记录细节。希望这篇B-CNN的实现过程能帮你在层级分类任务上少走几圈,尤其是分叉点选择、损失配比和推理融合这三块,多花一点时间是值得的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询