资讯动态

CRN级联细化网络源码解析:从棋盘检测到语义分割的通用框架

发布时间:2026/10/21 5:38:05 来源:尧图企业网站定制
1. CRN框架的设计思路拆解先交代一下背景。CRNCascaded Refinement Network级联细化网络不是某一个库的专属名词它本质上是一种“由粗到精”的视觉任务处理范式。我最早接触的这份CRN源码是做棋盘角点检测和机械臂抓取位姿估计用的后来我把这套框架的思想迁移到了语义分割任务上效果也相当能打。你要是手头有类似的密集预测任务——分割、边缘检测、棋盘识别、关键点定位——这套框架的代码结构都值得啃一遍。1.1 CRN到底解决了什么问题传统的分割或者检测网络走的是“Encoder-Decoder”这条老路编码器把分辨率一路降下去拿到足够大的感受野解码器再一层层把分辨率恢复回来。这个思路本身没问题但“恢复分辨率”这个动作本质上是在补信息——补出来的信息是网络猜的不是真的。CRN换了一个角度它不再尝试从高维特征里“变出”丢失的空间细节而是让网络先输出一个全局结构正确、但分辨率很低的结果然后用一个级联的细化模块在每一级对上一步的预测结果做“修正”而不是“重建”。这个区别很关键。重建是发散式的修正则是收敛式的。每一级细化模块拿到的输入是上一级已经成型的预测图它只需要“挑毛病”——哪里边缘不够清楚哪里类别边界有锯齿哪里小目标被漏掉了——然后在这个基础上叠加一个残差修正。代码里对应着的就是类UNet的refinement unit输入输出通道数设计得非常克制参数增量极小。1.2 从这个角度回看代码结构我拿到这份源码的第一反应是去翻主网络的定义文件。整个框架的核心就两个关键词base预测 级联refinement。代码层面体现为主干网络backbone负责提取多尺度特征输出一个极低分辨率的初始预测通常是原图的1/8或者1/16一个循环调用的refinement模块把低分辨率预测逐步上采样同时通过可学习的残差模块修正预测图每一级的输入不仅包含上一步的预测图还把主干网络对应层的特征拼接进来相当于给了细化模块“参考原图”的机会。所以你在源码里看到的training循环往往不是简单地把一个网络从头跑到尾而是分阶段优化先让base模块收敛到全局结构大致正确的程度再固定base部分或减小其学习率让refinement模块专注细化边缘和细节。我实测中如果一开始就让所有模块以同样学习率同时训边缘质量会明显差一截原因就是base的梯度把refinement的修正方向带偏了。2. 源码目录结构与关键文件地图源码拿到手第一步不是看公式、不是跑demo而是先把目录结构捋清楚。我整理过一份注释版文件地图直接照着这份去读效率高得多。2.1 目录结构一览与每个文件的角色crn_project/ ├── configs/ # 所有实验配置yaml/json格式 │ ├── train_xxx.yaml # 训练配置数据路径、batchsize、学习率 │ └── eval_xxx.yaml # 评估配置模型权重路径、测试集、输出目录 ├── libs/ │ ├── models/ # 网络结构定义 │ │ ├── crnet.py # CRN核心网络定义base refine循环 │ │ ├── backbone/ # 各种主干网络实现 │ │ └── modules/ # GCNrefinement unit上采样模块 │ ├── datasets/ # 数据加载与预处理 │ ├── utils/ # 各类工具函数iou、可视化、权重初始化 │ └── core/ │ ├── train.py # 训练主流程 │ ├── eval.py # 评估主流程 │ └── config.py # 配置读取与默认值合并 ├── tools/ │ ├── train_net.py # 训练入口脚本 │ ├── eval_net.py # 评估入口脚本 │ └── create_data.py # 把原始数据转成训练所需的格式 ├── checkpoints/ # 训练权重存放 ├── outputs/ # 预测图、日志、tensorboard文件 └── requirements.txt真实项目里可能还会多几个文件但核心骨架八九不离十就是这个样。理解源码的时候优先读crnet.py和train.py这两个文件其余的都是“服务类”的辅助代码。config.py 里的默认值合并逻辑也值得看因为CRN的训练参数跨阶段可能不一样——base阶段和refine阶段的epoch数、学习率、损失权重很多作者习惯直接在yaml里写死而不是写进代码。如果你只改了代码没改yaml很大概率你改的东西根本没生效。2.2 配置系统为什么这么设计我把训练配置单独拎出来说是因为CRN这套代码的配置设计有一个容易踩坑的特点配置是分层的而不是扁平的。# 举个train_xxx.yaml的片段例子 base: backbone: resnet18 pretrained: true freeze_bn: false refine: iter_num: 3 # 细化迭代次数源码里常写作T refine_channels: 64 use_gcn: true data: root: ./data/xxx train_split: train.txt loss: base_weight: 1.0 refine_weight: [0.5, 0.3, 0.2] # 每一级细化损失的衰减权重 optim: lr: 0.001 epochs_base: 60 epochs_refine: 60注意loss配置里那个refine_weight列表它的长度必须跟iter_num对上。我见过有人把iter_num改成5但忘记扩充这个列表结果训练到一半直接IndexError。这种设计背后是有讲究的每一级refinement的置信度不一样越靠后的细化输出理论上应该越好所以源码默认给后面的refine输出更高的权重。但实际训练中我发现一级细化就比二级细化的提升大得多到了第三级边际收益已经很小。这个权重列表是作者调试出来的经验值你要是换数据集最好重新验证一遍每级输出的单独精度再决定要不要沿用。3. 网络骨架与核心模块源码详解打开 crnet.py你会看到一个典型的PyTorch风格网络定义。整体可以拆成三块主干特征提取、初始预测生成、级联细化循环。我逐个讲。3.1 主干网络与初始预测生成CRN的base部分通常采用的是残差网络作为主干输出一个1/8分辨率的特征图然后接一个1x1卷积把通道数压缩到类别数得到初始预测。这里有一个很多人容易忽略的细节源码里base的输出不接Softmax直接输出logits而refinement阶段的输入却是上一级的softmax结果。def forward(self, x): # 主干特征 feat self.backbone(x) # 1/8分辨率特征 base_out self.base_head(feat) # [N, C, H/8, W/8] 初始logits prev_pred torch.softmax(base_out, dim1) # 转成概率图供refine使用 pred_list [base_out] # 级联细化循环 for i in range(self.iter_num): refined self.refine_units[i](prev_pred, feat) # 这里refined是残差不是直接输出概率图 new_logits F.interpolate( refined, sizeprev_pred.shape[-2:], modebilinear, align_cornersTrue ) pred_list.append(new_logits) prev_pred torch.softmax(new_logits, dim1) return pred_list这段逻辑读起来简单但它藏了两个听重要的设计决定。第一refinement unit输入的不是特征图而是上一级的预测概率图这保证了网络每一级都在做“分类结果的修正”而不是做“高维特征的再编码”第二每一级refine输出的其实是一个残差量跟上一级预测相加之后才得到新的logits这跟ResNet的残差思想一脉相承。我稍微改过这个结构尝试让refine直接输出完整logits效果反而变差——网络学出来的残差基本是零均值的小量比直接预测大 logits 稳定得多。3.2 GCN模块类别关系的全局上下文编码这份CRN源码里还有一个值得单独说的模块一个基于图卷积的上下文模块有些分支实现可能叫CGM即Context Graph Module。它的作用是在refinement的某一级插入一个全局的类别关系建模class GraphConvolution(nn.Module): def __init__(self, in_features, out_features): super().__init__() self.fc nn.Linear(in_features, out_features) self.norm nn.LayerNorm(out_features) def forward(self, x, adj): # x: 类别节点特征 [B, C, D] out torch.matmul(adj, x) # 邻接矩阵聚合邻域特征 out self.fc(out) return self.norm(out) def get_class_adjacency(cls_feat): # 计算类别特征间的相似度矩阵类似全连接图的邻接矩阵 cls_feat_norm F.normalize(cls_feat, dim1) adj torch.matmul(cls_feat_norm, cls_feat_norm.transpose(1, 2)) return adj这个模块的基本思路是先用全局平均池化把当前预测图的每个类别分量压成一个特征向量把这些类别向量看成一个全连接图上的节点节点之间用相似度构建邻接矩阵然后通过图卷积让类别信息互相传播最后把传播后的特征重新映射回每个像素位置作为附加的“上下文线索”拼回去。理解这个模块的关键是它学习到的是“类别共现关系”。比如车这个类别和路这个类别经常一起出现那么通过图卷积后看到路就会给车一个弱的先验激活。这个能力对于棋盘检测这种类别极少的任务用处不大但对于语义分割数据集这种几十个类别的场景收益非常明显。3.3 细化单元内部构造源码里的refinement unit是一个轻量级残差块内部往往是两层卷积BNReLU的堆叠。它有两个输入一个是上一步预测的概率图上采样到当前分辨率另一个是主干网络对应层的特征。两者先拼接再通过1x1卷积融合通道之后过两个3x3卷积修正空间细节。这里要提醒一点不同版本的CRN源码iter_num的循环可能会共享同一个refine模块也可能是多个独立的refine模块。共享版本参数少、训练快但每一级使用的其实是同一个修正器容易在上级特征和本级特征之间打架独立版本参数多但每一级能针对自己的尺度学专门的修正模式。我测下来独立版本在语义分割上平均能高1-2个点mIoU代价是显存变多、训练变慢。如果你的显存只有11GB级别建议先跑共享版本确认流程通再去试独立版本。3.4 训练循环中的阶段切换与损失组合CRN训练流程的经典做法是两阶段训练后面会有命令行说明。阶段一只训练backbone和base_head用的损失是base_out的交叉熵阶段二冻结backbone的早期层或者整体调低学习率主要训练GCN和refine部分此时的损失是base损失和每一级refine损失的加权和。loss_dict {} loss_dict[base_loss] criterion(base_out, target) for i, refined_logits in enumerate(refined_outputs): # 上采样到原图分辨率再算loss refined_logits F.interpolate(refined_logits, sizetarget.shape[-2:], modebilinear, align_cornersTrue) loss_dict[frefine_{i}_loss] criterion(refined_logits, target) * refine_weights[i] total_loss sum(loss_dict.values())有个细节源码里refine损失用了权重衰减但衰减不是线性也不是等比而是越晚的refine输出权重越大。第一级refine的权重通常只有0.3左右最后一级可能1.0。这个设计的理由是越后面的refinement承担“最终输出”的责任理应给更大的梯度。但如果你训练后期发现底层的refine模块没啥用了也可以直接把前面的refine权重降到零。4. 跑通源码的实操记录与避坑指南源码光看是不够的尤其是CRN这种带stage切换的框架不实际跑一遍很难理解训练日志里那些loss变化曲线。我第一次跑这份代码花了整整两天才把流程完全走通其中遇到的坑不少。这里把我自己的完整操作过程和一些踩坑心得整理一下。4.1 环境准备与数据格式转换环境上这份源码对PyTorch版本没有特别苛刻的要求我实测在PyTorch 1.10到2.1之间都能跑但有两个依赖要单独注意一个是tensorboard日志和loss曲线都靠它输出另一个是opencv-python很多数据预处理脚本依赖它的图像编解码接口。数据格式方面CRN训练通常要求把标注图片转成索引图并且目录里必须有一份txt文件记录每张训练图的路径。源码自带一个create_data.py脚本但如果你用的是自己的数据集我建议自己写个转换脚本import os from PIL import Image import numpy as np def convert_label_to_index(src_path, dst_path, class_to_id): mask np.array(Image.open(src_path).convert(RGB)) h, w mask.shape[:2] out np.zeros((h, w), dtypenp.int64) for color, cls_id in class_to_id.items(): match np.all(mask np.array(color), axis-1) out[match] cls_id Image.fromarray(out.astype(np.uint8)).save(dst_path)注意索引图保存格式最好还是PNG不要用JPEG——JPEG压缩会让类别边界产生伪影训练出来的模型会在边缘上疯狂预测错误。4.2 逐步训练从base到refine的完整命令训练入口脚本通常接受一个--config参数它会读取yaml里的配置并自动分出训练阶段。命令行操作一般是这样# 阶段一训练base部分 python tools/train_net.py --config configs/train_xxx.yaml --stage base # 阶段二加载阶段一权重训练refineGCN python tools/train_net.py --config configs/train_xxx.yaml --stage refine \ --load checkpoints/xxx_base.pth # 评估 python tools/eval_net.py --config configs/eval_xxx.yaml \ --load checkpoints/xxx_refine.pth有时候你翻源码发现train_net.py里面并没有显式的stage参数那大概率是通过yaml里的train_phase字段控制的。我建议你动手改代码前先搜索一下代码里epochs_base和epochs_refine这两个字符串能快速定位stage的切换逻辑。关于batch sizeCRN对显存占用其实不算夸张但refinement阶段因为需要同时存base特征、多级预测和梯度显存占用会明显上升。我在8GB显存的卡上512x512输入、iter_num3的情况下只能跑batch size4。如果你内存不足优先把输入尺寸缩到384x384效果基本不变显存立马降下来。4.3 训练日志怎么看loss曲线与阶段切换的表现training日志里base阶段的loss降得很快一般20个epoch就能压下来。但到了refine阶段你会看到总loss呈现一种“震荡回升”的现象——先突然上涨然后缓慢下降。千万别以为模型坏了这是正常的因为阶段切on的时候新增了refine损失和GCN模块它们是从零开始学的总loss自然会波动一阵子。真正需要警惕的情况有两种base loss 在refine阶段不再下降甚至上升说明refine模块的梯度干扰到了base的特征提取。解决方法就是冻结backbone的前几层或者把backbone的学习率调成refine模块的0.1倍。refine loss 只降了一点点就停滞大概率是iter_num设置得太深而每一级refine模块的容量不足以处理前一级的残差这种情况减少iter_num反而有提升。4.4 常见报错与排查速查表我把跑源码过程中真实遇到过的几个报错放在一个表里读者可以对照排查。报错现象可能原因解决办法IndexError: index out of range出现在loss计算refine_weight列表长度与iter_num不一致检查yaml中两者是否对齐RuntimeError: CUDA out of memory出现在refine阶段多级预测图和特征同时驻留显存降低batch size或减少iter_num或关掉GCN训练loss一直是nan学习率过大或输入数据里有脏标注负标签-1调低学习率检查mask中是否出现负值推理结果整体偏移一个类别类别id与色彩映射表对不上检查create_data中的class_to_id映射每个batch训练时间特别长数据加载没有多线程或者依赖了瓶颈CPU预处理把数据预处理好存成npy关闭实时增强里的重缩放除了这些还有一个地方特别容易出问题多卡训练的环境变量。如果你用torch.distributed.launch启动训练但没有正确设置RANK、WORLD_SIZE这些环境变量代码会直接从--local_rank报错。我这边建议优先用单卡训练把逻辑跑通后面再考虑分布式。5. 把CRN框架迁移到你自己的任务上最后聊点框架层面的经验。整套CRN源码读完之后最有价值的收获不是某个模块的写法而是“由粗到精、逐级修正”这个设计思路在不同任务上的迁移套路。我自己就把它从棋盘检测搬到了语义分割和遥感地物分类上做法很简单保留backbone和base_head不变backbone可以换任何现成的分类网络refinement unit 的输入特征从“backbone最后一层特征”换成“backbone不同stage的特征组合”也就是像FPN那样建立特征金字塔连接GCN模块可有可无类别数小于10的任务基本上不需要加超过20类加上收益才明显损失函数可以和源码一样采用多级交叉熵加权相加任务如果有边界细化需求还可以增加一个边缘感知loss。这套源码里最值得借鉴的工程细节是对“阶段训练”的处理。它在训练脚本里用清晰的分段逻辑管理了base和refine两步训练并且每个阶段都能单独保存权重、单独评估。这个做法往小了说就是一份工程代码往大了说揭示了一个训练密集预测模型的通用原则先让网络学对“位置和结构”再让网络学“颜色和边界”。我的经验是这个顺序几乎适用于所有像素级预测任务。下一份源码解析我打算重点拆一下CRN的refinement unit内部空间维度和通道维度的变化细节以及GCN模块的反向传播行为。到时候见。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑