ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深度学习CNN核心路径:从卷积到目标检测与分割

深度学习CNN核心路径:从卷积到目标检测与分割 动手学深度学习这套材料我跟过不止一轮第 2450 集合这一段是我个人认为信息密度最高、也最容易半途放弃的部分。前面二十多个集合讲的是数据操作、线性回归、softmax 回归、多层感知机、模型选择、正则化和房价预测本质上是把深度学习当成带非线性的回归/分类来用张量形状最多也就到 批大小 × 特征数 这个量级。到了第 24 集合画风突变输入变成 224×224×3 的图像张量从二维变四维单层参数量从几十万跳到上亿还要开始跟坐标、锚框、转置卷积这些带几何含义的概念打交道。很多人就是在这里卡住的——不是数学不会而是形状对不上、参数算不清、结果不符合直觉。这份学习笔记覆盖第 24 到第 50 集合的完整路径从全连接层到卷积的动机推导卷积、填充、步幅、多通道、池化的算子细节到 LeNet、AlexNet、VGG、NiN、GoogLeNet、ResNet、DenseNet 这条现代卷积网络演进线再到图像增广、微调、边界框、锚框、IoU、NMS、SSD、语义分割与 FCN最后收在序列模型和文本预处理上为后面循环神经网络铺路。它解决的问题很具体让你知道每一集合的重点是什么、代码里每个参数为什么这么设、形状为什么这么变、跑不通时该往哪儿查。适合谁来跟写过一点 Python、碰过 NumPy但对 CNN 只有听过池化和卷积核这种程度的人正在准备深度学习岗面试、需要把 CNN 这条线讲清楚的人以及已经在用现成模型但想回头把底层算子和数据流补齐的工程师。我会把参数量、输出尺寸、阈值这些能算的都算给你看把踩过的坑标出来代码片段可以直接抄到自己的工程里跑。1. 第 2450 集合到底讲了什么先把地图铺开1.1 为什么这一段值得单独整理我在整理笔记时发现一个规律深度学习学习曲线上的断崖通常不在反向传播而在张量维度从二维升到四维的那一刻。二维张量你还能靠直觉想象成表格四维张量 批大小 × 通道 × 高 × 宽 就只能靠习惯和肌肉记忆了。第 24 集合正好是这条断崖的起点它把全连接层为什么在图像上不可行这件事从参数量角度讲透然后引出卷积。从这个节点往后你面对的每一个新概念——多通道、步幅、池化、批归一化、残差连接、锚框、转置卷积——都在改变数据的组织方式。我把 2450 单独拎出来还有一个很实际的理由这 27 个集合是整本书里唯一一段可以完整串成一条工程主线的内容。前半部分偏原理玩具数据后半部分的注意力机制、优化算法、多 GPU 训练又各自独立成块。只有 2450 这一段从图像怎么进网络到网络怎么设计到检测框怎么生成到上采样怎么还原再到序列怎么组织成批是一条能画在纸上的完整链路。把这条链路自己画一遍比抄十遍公式有用。另外提醒一句这一段有大量看起来会了、一动手就错的内容。比如填充的公式你会背但真给你一个 224、卷积核 7、步幅 2 的配置让你算输出是多少很多人第一反应还是除不尽怎么办。所以我在笔记里尽量把每个公式都配一组具体数字。1.2 27 个集合的内容地图下面这张表是我按自己的笔记顺序整理的集合编号是我这套学习计划里的序号主题和核心概念对应到书里的章节。这个映射不是唯一解不同人的学习计划切分方式会不一样但主题先后顺序基本是固定的。集合区间主题核心概念我实际花的时间最常踩的坑2428卷积神经网络基础全连接到卷积、二维互相关、填充步幅、多通道、池化6 小时输出尺寸算错、通道顺序搞反2930早期经典网络LeNet、AlexNet3 小时输入尺寸不匹配、ReLU 位置写错3133块式与并行结构VGG、NiN、GoogLeNet5 小时1×1 卷积作用理解不到位3436归一化与跳过连接批量规范化、ResNet、DenseNet7 小时BN 的 train/eval 模式、通道拼接维度3738迁移学习图像增广、微调3 小时验证集错误地做了增广3943目标检测基础边界框、锚框、IoU、NMS、多尺度8 小时坐标系混用、锚框数量爆炸4445检测模型R-CNN 系列、SSD4 小时损失函数权重难调4648分割与上采样语义分割、转置卷积、FCN5 小时转置卷积输出差 1 像素4950序列与文本序列模型、文本预处理、语言模型4 小时词表与未知词处理从时间分布上看卷积基础和现代网络演进占了将近一半目标检测占了接近三分之一这跟实际工作中的需求分布也基本吻合——大部分视觉项目是在做检测和分割而检测模型的核心组件全在 2428 里。1.3 三条起跑线你该从哪一集合插进去如果你已经会写 PyTorch 的 MLP别从第 24 硬啃。可以直接从第 30 集合AlexNet切进去先在脑子里建立一个能跑起来的完整 CNN的样板再回头补第 2428 的算子细节。我当时就是反过来走的先啃算子啃了三天越看越抽象后来先跑通 AlexNet再回看互相关运算十分钟就通了。先有整体再抠细节比死磕细节效率高得多。这个顺序上的取舍很关键因为它决定了你是在已有框架里插知识还是从零凭空搭框架。只关心目标检测的话起点挪到第 39 集合但必须把第 27 集合多输入多输出通道和第 28 集合池化层补上否则你会看不懂特征图尺寸是怎么逐层递减的而后面的锚框尺寸、多尺度预测全依赖这个递减规律。我见过有人直接跳去读 SSD 的代码卡在为什么第 4 层特征图是 38×38、第 7 层是 19×19这个问题上整整一天其实就是池化和步幅的累积效果没算清楚。如果你的数学基础偏弱先别急着补泛化误差界那类理论把时间花在形状推导和参数核算上。这一段里 90% 的报错都和数学无关是纯粹的维度问题。真正需要线性代数的地方只有一处理解卷积是一种稀疏的、权重共享的线性变换。这句话你可以在纸上用 4×4 的输入和 2×2 的核手算一遍矩阵形式算完就明白为什么卷积比全连接的参数量少一个数量级而且这个理解会一直用到后面的自注意力和 Transformer 那部分。2. 从全连接到卷积参数量算一遍就懂了2.1 全连接层在图像上的参数爆炸先做一道我每次讲这个都要做的算术题。假设输入是一张 224×224 的彩色图展平之后是 150528 维向量。第一个隐藏层只有 1024 个神经元全连接层的权重矩阵就是 150528 × 1024也就是 154140672 个参数一亿五千多万。这还只是一层。再加一层 1024 × 1024又是 104 万参数看起来不多但这层已经和图像的空间结构完全脱钩了。真正的问题不是参数量本身大而是这种参数量的增长方式不合理。图像有一种性质把照片整体向右平移三个像素它还是同一张照片。但全连接层对每个位置都有独立的权重输入平移三像素之后激活值全变了模型必须重新学一遍。等于说同一只猫在左上角和右下角模型要学两套参数。数据量不够的时候这就是必然过拟合。反过来看卷积一个 5×5 的核处理三通道输入只占 5×5×3 加上偏置共 76 个参数无论图像多大都只有这 76 个。参数量少了六个数量级而且因为权重在所有位置共享平移后响应也跟着平移天然具备平移等变性。2.2 平移不变性与局部性卷积的两个前提假设卷积能work靠的是两个假设。第一个是平移不变性说的是判断某个模式是否存在不该依赖它在图像中的位置。第二个是局部性说的是判断一个小小的区域里有什么只需要看这个区域附近的像素远处的像素可以先不管。这两个假设都不是凭空来的它们是从图像本身的统计特性里总结出来的属于归纳偏置的典型应用——你用先验知识给模型减了自由度模型需要的样本量就下来了。从数学上看全连接层写成 $h_{i,j} \sum_{a,b} v_{i,j,a,b} x_{a,b}$意思是从输入任意位置 (a,b) 到输出任意位置 (i,j) 都有一个独立权重。要满足平移不变性就得让这个权重只跟相对位移有关写成 $h_{i,j} \sum_{a,b} v_{i-a,j-b} x_{a,b}$这就成了卷积的形式。要满足局部性就让当 |a| 和 |b| 超过某个范围时 $v$ 直接取零也就是核的大小。两步约束下去一亿五千万参数变成 76 个代价是模型表达能力下降但对图像任务来说这个交换极其划算。2.3 手写二维互相关理解卷积其实是互相关这里有个概念必须先掰清楚否则后面看文档永远困惑。框架里的卷积层做的是互相关不是数学意义上的卷积后者要把核翻转 180 度。因为核是学出来的翻转不翻转等价于换一组参数所以实现上直接省掉翻转这一步不影响结果。import torch from torch import nn def corr2d(X, K): 二维互相关运算X 是输入张量K 是核张量 h, w K.shape Y torch.zeros((X.shape[0] - h 1, X.shape[1] - w 1)) for i in range(Y.shape[0]): for j in range(Y.shape[1]): Y[i, j] (X[i:i h, j:j w] * K).sum() return Y这段双重循环是我建议每个初学者都手敲一遍的代码。写完再对照nn.Conv2d的输出验证一次输入torch.ones(1,1,5,5)核torch.tensor([[1,0],[0,-1]], dtypetorch.float32)手算结果是 4×4 的全零张量。这个验证动作看起来很笨但它能帮你确认一件事——你脑子里的滑动窗口相乘求和和框架实现是同一个东西。后面遇到空洞卷积、分组卷积时你还能靠这个心理模型自己推导输出形状。2.4 填充、步幅与输出尺寸一条公式走天下输出尺寸的公式只需要记一条但要记全$$n_{out} \left\lfloor \frac{n_{in} - k 2p}{s} \right\rfloor 1$$其中 n 是输入边长k 是核边长p 是单侧填充行数s 是步幅向下取整。我拿三组真实配置算一遍你跟着对一次就不会忘了。配置一输入 224核 5填充 2步幅 1结果是 (224-54)/11 224尺寸不变。配置二输入 224核 7填充 3步幅 1结果是 (224-76)/11 224同样不变。配置三输入 224核 5填充 2步幅 2结果是 (224-54)/21 112.5 向下取整为 112再加 1 得 113这里要注意顺序先算除法取整再加一(224-54)/2 111.5取整 111加 1 得 112。正好减半。注意填充的目的是控制输出尺寸保持分辨率常用 p (k-1)/2前提是 k 为奇数且 s 1。核尺寸基本都是奇数原因不是数学必需而是奇数核有唯一的中心点讨论中心像素和锚点对齐时不会出现几何歧义。这在后面讲锚框和特征图对齐时会直接影响你的坐标换算。还有一个坑当 (n - k 2p) 不能被 s 整除时不同框架或不同版本的取整行为可能让你多一列像素。我在做特征图与原始图像坐标映射时就因为这里差了一个像素导致画出来的框整体偏了半格。稳妥的做法是让所有配置都整除或者写一个断言把每层的输出形状打印出来对一遍。3. 通道、1×1 卷积与池化把积木拼成 LeNet3.1 多输入多输出通道的参数量核算单通道卷积只是个教学模型真实网络里每一层都是多通道进、多通道出。规则很简单输入的通道数决定了每个卷积核的厚度输出的通道数决定了有几个卷积核。也就是输入通道 c_in输出通道 c_out核尺寸 k×k那么权重张量形状是 (c_out, c_in, k, k)参数量为 c_out × c_in × k × k c_out加偏置。算一组真实数字输入 3 通道输出 64 通道核 7×7参数量是 64×3×7×7 64 9408 64 9472。同样规模如果用全连接3×224×224 到 64 个神经元参数量是 150528 × 64 9633792差了三个数量级。第二组3×3 卷积输入 64输出 128参数量是 128×64×9 128 73728 128。你会发现随着通道数翻倍参数量增长是平方级的这就是为什么现代网络里 3×3 卷积特别多而 5×5、7×7 通常只在第一层用一次用来快速吃掉空间分辨率。3.2 1×1 卷积到底在干什么1×1 卷积刚看会觉得莫名其妙核只有 1 个像素能提取什么特征答案是它根本不看空间邻域只做通道之间的线性组合。输入 c_in 个通道在同一空间位置上取值乘上权重求和得到 c_out 个新通道值。空间尺寸完全不变信息只在通道维上重新分配。它的两个实际用途都很关键。第一是降维比如 256 通道降到 64 通道能省下四倍的后续计算量第二是增加非线性1×1 卷积后面接 ReLU相当于在每个像素位置上做了一次全连接加激活但参数量只有 c_out × c_in没有空间维度参与。NiN 用 1×1 卷积替代全连接层GoogLeNet 的 Inception 模块用它做瓶颈层控制计算量ResNet 的瓶颈块里也是先 1×1 降维、再 3×3 卷积、再 1×1 升维。可以说 1×1 卷积是这段内容里性价比最高的一个知识点。提示判断该不该用 1×1 卷积降维我的经验法则是看通道数是否超过 256。超过这个量级先降维再做大核卷积通常总计算量能砍掉一半以上而精度损失在 1% 以内。3.3 池化层的取舍与反传细节池化层不带参数只做下采样。2×2、步幅 2 的最大池化把特征图边长减半。它有两个作用一是降低后续层的计算量二是扩大感受野让后面层的每个神经元看到更大的原始区域。最大池化和平均池化的区别在于前者保留局部最强响应后者保留整体的平均强度。视觉任务里最大池化用得更多因为某个特征出现了没有这件事比特征的平均强度更重要。反传的细节值得单独说一下。最大池化在反向传播时只把梯度回传给前向时选中最大值的那个位置其余位置梯度为零所以它相当于一个位置选择器。平均池化则把梯度平均分配到窗口内所有位置。这个差别在调试梯度时很有用如果你打印梯度发现某些位置永远是零先确认一下那是不是最大池化没选中的像素不一定是 bug。另外现代网络里池化的地位在下降很多结构用步幅为 2 的卷积代替池化来完成下采样。好处是下采样过程也带参数、可学习坏处是参数量和计算量上升。我在小数据集上做对比时把这个替换一般会带来 0.3 到 1 个点的精度提升但同时训练时间增加 20% 左右看你的算力预算决定。3.4 LeNet 完整实现与训练现场记录LeNet 是第 29 集合的主角它小到可以在单卡上几分钟跑完但结构完整是理解 CNN 的样板。简化版本的结构是5×5 卷积 6 通道2×2 池化5×5 卷积 16 通道2×2 池化展平后接三个全连接层。import torch from torch import nn net nn.Sequential( nn.Conv2d(1, 6, kernel_size5, padding2), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Conv2d(6, 16, kernel_size5), nn.Sigmoid(), nn.AvgPool2d(kernel_size2, stride2), nn.Flatten(), nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(), nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 10) )注意输入是 28×28 单通道第一个卷积加了 padding2 把尺寸维持在 28池化后 14第二个卷积不加填充变成 10池化后 5所以展平维度是 16×5×5 400。这个 400 一定要自己算一遍写错就是形状不匹配报错。训练侧我用的是 SGD学习率 0.9每三轮乘 0.1 衰减批大小 256十轮左右在 Fashion-MNIST 上能到 0.82 左右的准确率。现场记录里有个值得注意的现象训练损失下降到 0.2 以下时验证准确率基本就停在 0.82 附近不动了。这不是代码问题是 LeNet 容量就这么大加上当时用的是 Sigmoid 和平均池化梯度偏弱。想看更漂亮的曲线就把 Sigmoid 换成 ReLU、平均池化换成最大池化同一份数据上能提到 0.88 左右。这个对比实验我强烈建议做一次两个改动各带来多少提升你会有直观感受也正好接上第 30 集合要讲的 AlexNet。4. 现代卷积网络演进线从 AlexNet 到 DenseNet4.1 AlexNetReLU、Dropout 与数据增广三板斧AlexNet 相比 LeNet 的改进书上列了四条但我认为真正起决定作用的是三条ReLU、Dropout、数据增广。ReLU 把负半轴梯度为零正半轴梯度恒为 1直接绕开了 Sigmoid 在饱和区梯度趋零的问题这也是它能训到八层的根本原因。Dropout 在训练时随机把一部分神经元置零相当于每次迭代都在训练一个子网络集成效应抑制过拟合。数据增广靠随机翻转、随机裁剪、颜色抖动把有效数据量翻了好几倍。结构上有两个细节要留意。第一层用的是 11×11、步幅 4 的大核这个设计的目的是快速把 224 的输入降到 55省掉大量计算。放到今天来看用几层 3×3 堆叠代替一个大核效果会更好但当时算力有限大核是效率妥协。第二是它把网络拆到两块 GPU 上跑所以中间有个通道拼接操作你现在单卡实现时不需要管这个直接当成一个连续通道即可但读代码时看到奇怪的分支别慌。4.2 VGG 与 NiN块式设计和全局平均池化VGG 的贡献是把网络结构抽象成了块一个 VGG 块由若干相同的 3×3 卷积同样填充保持尺寸加一个 2×2 最大池化组成后面的块把通道数翻倍、分辨率减半。这种设计让代码从手写每一层变成用循环生成块的组合第一次让网络深度可以靠配置参数控制。参数量上有个反直觉的点VGG 的绝大部分参数其实在最后三个全连接层里卷积部分反而很轻。VGG11 大约 1.3 亿参数其中全连接层占了 1.2 亿以上。NiN 就是冲着这个痛点来的。它提出在卷积层后面接 1×1 卷积作为微型全连接最后用一个全局平均池化把每个通道压成一个数直接输出类别得分彻底去掉全连接层。参数量因此大幅下降而且因为不再有固定尺寸的全连接层理论上可以接受任意尺寸输入。这个思路后来被证明非常有用它解决了全连接层参数冗余的问题但也带来一个后果通道数直接等于类别数通道间的可解释性变强了灵活性变弱了。在实际工程里我一般会保留一个小的全连接层兼顾参数量和灵活性。4.3 GoogLeNetInception 多分支与计算量控制Inception 模块的核心想法是不用猜某个位置该用 3×3 还是 5×5四个分支并行做最后在通道维拼接让网络自己学该信哪个。四个分支分别是 1×1 卷积、1×1 接 3×3、1×1 接 5×5、3×3 最大池化接 1×1。直接这么写计算量会爆炸所以在 3×3 和 5×5 之前都加了 1×1 做通道压缩。我算过一组对比能看出瓶颈层的价值。假设输入 192 通道输出也要 192 通道。直接用一个 5×5 卷积参数量是 192×192×25 921600。改成先 1×1 降到 16 通道再用 5×5 升回 192参数量是 192×16 16×192×25 3072 76800 79872只有原来的 8.7%。精度上因为多了一层非线性反而常常更好。这就是 1×1 卷积在工程里的真实价值它让多分支并行这种昂贵结构变得可负担。GoogLeNet 一共 22 层带参数层参数量却只有 VGG 的十分之一左右靠的就是这种密集的通道压缩。后来 Inception 系列加上了批归一化和因式分解卷积把 5×5 拆成两个 3×3但核心思想没变。4.4 批量规范化最容易踩的四个参数坑批量规范化BN在第 34 集合它的原理不复杂对每个通道在一个小批量内算均值和方差把激活值标准化到零均值单位方差再用两个可学习参数做缩放和平移。真正难的是工程细节我在这里踩过至少四次坑。第一个坑是 train 和 eval 模式必须切换。训练时 BN 用当前批次的统计量同时用移动平均维护一份全局统计量推理时用的是这份移动平均。如果你在验证阶段忘了model.eval()验证结果会随批大小波动且明显偏低。第二是批大小不能太小批大小为 1 时方差无意义整个批次被标准化成 0模型直接崩。经验下限是 16最好 32 以上检测任务里因为显存限制批很小这时候要换成同步 BN 或者干脆用 GroupNorm。第三是 momentum 参数默认 0.1 指的是新统计量的权重越小更新越慢越稳如果你的数据集分布和预训练模型差异大把它调到 0.01 会更平滑。第四是 BN 层的位置放在卷积之后、激活函数之前这是原论文的做法。注意微调预训练模型时如果你冻结了前面的卷积层BN 层也要一并冻到 eval 模式。否则即使卷积权重没变BN 的统计量还是会被你的小批量数据带偏前面层的输出分布整体漂移微调效果会大打折扣。这个问题排查起来很隐蔽因为损失曲线看起来只是收敛得慢一点。4.5 ResNet 与 DenseNet跳过连接为什么能救深层网络理论上网络越深表达力越强但实验结果连续几年反着来20 层比 56 层好。这不是过拟合导致的训练误差本身就更高说明是优化出了困难深层网络在反向传播时梯度被反复连乘叠加 BN 之后依然很难传到前面几层。ResNet 的解决办法极其简单让每个块学残差 f(x)输出 f(x) x。如果这一层没什么可学的把 f(x) 学成零整体就退化成恒等映射至少不会变差。梯度因为那条直通的加法支路可以直接传回去不用穿过所有卷积层。这里有个公开的误解要纠正残差连接解决的不是梯度消失而是深层网络的退化问题。梯度消失是现象退化是那个加法支路要解决的优化问题。当然恒等映射的设计同时也顺手缓解了梯度消失问题工程上这两者往往同时受益。DenseNet 把相加改成拼接每一层的输入是前面所有层输出的通道拼接。好处是特征复用充分、梯度路径更短而且每层只需要产生很少的新通道增长率通常设 12 到 32。代价是显存占用高因为拼接后的特征图要一直留着。我在显存受限的设备上试过 DenseNet同样的深度比 ResNet 多吃 30% 到 50% 的显存这是它没能完全取代 ResNet 的主要原因。实现层面ResNet 的瓶颈块顺序是 1×1 降维、3×3 卷积、1×1 升维最后相加前要确认通道数一致不一致时需要在跳连上加一个 1×1 卷积做投影。这个use_1x1conv开关是必踩的一个点忘了加就是形状不匹配报错。5. 计算机视觉实战增广、微调与目标检测5.1 图像增广几个必须记住的禁用场景图像增广是性价比最高的一招不增加任何推理成本只花训练时间。常见操作有随机水平翻转、随机裁剪加缩放、颜色抖动亮度、对比度、饱和度、色调、以及把多张图线性混合的 mixup 和 cutmix。原理上翻转和裁剪对应物体位置和朝向不该影响类别判断这个先验颜色抖动对应光照和成像设备差异不该影响判断这个先验。坑在于不是所有任务都能翻转。水平翻转对猫狗分类是无害的但对识别数字6 和 9 会互换、识别字母和文字、判断左右手方向、识别棋盘类对称结构就会直接引入错误标签。垂直翻转的适用范围更窄除了遥感图像和显微镜图像一般不用。裁剪也一样如果图像的关键信息本来就在边缘比如工业质检里的边缘缺陷随机裁剪会切掉目标模型学到的全是噪声。还有两个操作纪律增广只在训练集上做验证集和测试集一律用原图加标准预处理缩放加中心裁剪。另外增广的强度要跟着训练轮数看如果训练损失明显高于验证损失说明增广过强模型欠拟合该减弱如果验证损失早早抬头说明增广不够可以再加。5.2 微调冻结哪些层、学习率怎么给微调是第 38 集合的内容实际工作中的使用频率远高于从零训练。标准做法是拿一个在大数据集上训好的模型把最后的输出层换掉输出维度改成你的类别数前面的卷积层用预训练权重初始化然后分两组学习率训练。靠近输入的层学的是边缘、纹理这种通用特征变化要小靠近输出的层学的是语义组合可以大步调整。我给学习率的经验值是新加的输出层用 1e-3 到 1e-4中间层用 1e-4最底层的几层设成 0 或者直接冻结。冻结的时候注意前面提到的 BN 坑还要注意数据量少的时候每类几十张冻结层数要多数据量大的时候每类上千张可以把冻结层数减少甚至只用预训练权重做初始化。判断该冻结几层我有个实测有效的办法先冻结全部卷积层只训分类头跑五轮看验证准确率再把最后一个卷积块解冻再跑五轮。如果第二次准确率提升超过 2 个点说明还可以继续往上解冻如果提升不到 0.5 个点就停在第一次的配置省时间也更稳。5.3 边界框、锚框与 IoU坐标系混乱是头号杀手从第 39 集合开始进入目标检测这里最大的坑不是算法是坐标。同一张图上的一个框至少有三种表示法角点式 (x1, y1, x2, y2)、中心宽高式 (cx, cy, w, h)、以及带不带归一化除以图像宽高。角点式还分包含边界和不包含边界两种约定差一个像素。如果你的可视化框总是偏一点八成就是这里。我现在的做法是内部统一用归一化的中心宽高表示因为锚框的生成、偏移量回归、不同尺度特征图的坐标缩放在这个表示下公式最干净只在可视化和导出结果时转成角点式。转换公式写在一个工具函数里全流程只调用它绝不手写换算。这个约定帮我省掉了大量调试时间。IoU 是衡量两个框重叠程度的指标公式是交集面积除以并集面积。计算时有一个容易忽略的点交集宽高要取 max(0, ...)也就是两个框不相交时交集必须是零不能出现负数。我见过因为这个没夹紧导致 IoU 变成负数、进而 NMS 行为完全错乱的案例。5.4 NMS 与多尺度检测的阈值调法非极大值抑制NMS的逻辑是把所有预测框按置信度从高到低排序取出最高的那个然后删掉与它 IoU 超过阈值的所有框重复直到没有框剩下。阈值是关键参数设 0.5 会删得比较狠重叠的同类物体比如挤在一起的人容易漏检设 0.7 保留得多同一个物体可能出好几个框。通用检测任务我用 0.5密集场景用 0.6 到 0.7。多尺度检测解决的是物体大小差异问题。做法是在不同分辨率的特征图上都铺锚框分辨率高的浅层特征图负责小物体分辨率低的深层特征图负责大物体。锚框的尺寸和长宽比需要按数据集调我一般会先统计训练集里所有真实框的宽高分布画出直方图然后选覆盖 80% 数据的几组尺寸作为锚框基准。这一步手工调参的效果比盲目在默认值上试要快得多。锚框数量也不要贪多每个位置 3 到 5 个尺寸、3 个长宽比就是 9 到 15 个再往上正负样本严重失衡训练反而更慢。5.5 R-CNN 系列与 SSD 的工程差异R-CNN 系列走的是两阶段路线。第一代 R-CNN 用选择性搜索生成候选区域每个区域单独过一遍网络慢到没法用。Fast R-CNN 改成整张图先过一次骨干网络再在特征图上取候选区域对应的部分速度快了几十倍。Faster R-CNN 用区域提议网络RPN替代选择性搜索把候选框生成也变成可学习的一步成为两阶段检测的经典架构。SSD 是单阶段路线的代表直接在多个尺度的特征图上预测类别和框偏移一次前向出结果速度优势明显。两者的取舍很清楚两阶段精度略高、速度慢适合对精度敏感的离线场景单阶段速度快适合实时场景。工程上还有一个差异容易忽略——两阶段的损失函数是分类加回归两部分的加权和权重的调节对结果影响很大我一般从 1:1 开始如果框的位置明显不准就把回归权重往上调单阶段因为有大量的背景锚框需要引入难例挖掘或者焦点损失来平衡正负样本。5.6 语义分割、转置卷积与 FCN第 46 到 48 集合讲分割。语义分割要求输出和输入分辨率一致每个像素都有一个类别标签所以需要上采样。转置卷积是最常用的上采样手段但一定记住它不是卷积的逆运算它只是通过填充和滑动把特征图放大具体数值不对应原图。输出尺寸公式是 $n_{out} (n_{in} - 1) \times s - 2p k$跟卷积的公式不一样。举个例子输入 8核 3步幅 2填充 1输出是 (8-1)×2 - 2 3 15。如果填充设成 2输出就是 13。这 2 像素的差异会让最终分割结果和原图对不齐边界处出现系统性偏移。FCN 的做法是把分类网络的最后几层全连接换成 1×1 卷积这样网络可以接受任意尺寸输入并保持空间结构最后用转置卷积上采样回原分辨率。工程上有个技巧直接上采样 32 倍会非常粗糙把中间层和浅层特征图也上采样后相加能得到更精细的边界这就是跳跃连接在分割里的用法。我在小数据集上试过加上跳跃连接后边界区域的 IoU 能提升 5 个点以上比换更深的骨干网络划算。6. 序列模型与文本预处理为 RNN 铺路6.1 自回归、马尔可夫假设与因果关系第 49 集合从序列数据切入。序列和图像最大的区别在于长度可变、且有顺序依赖样本之间不再独立同分布。处理序列的基本框架是自回归用前 t 个时刻的观测预测第 t1 个。如果保留全部历史计算量和参数量都会随长度增长于是引入马尔可夫假设只依赖最近 τ 个时刻把问题变成固定输入的模型。这里还有一个容易混的概念是因果关系。预测当前时刻只能用过去的信息不能偷看未来这在训练和推理的实现上是有区别的训练时可以用教师强制把真实的前一时刻输入喂进去推理时只能把自己上一步的输出喂回来。两者的分布差异会导致推理阶段误差累积这是序列生成任务里一个通用的难题后面束搜索那一节会专门处理。6.2 文本预处理流水线从原始文本到索引张量第 50 集合是文本预处理看似简单但它是后面所有 NLP 任务的基础写错了后面全错。标准流水线是四步读入原始文本、词元化、构建词表、转成索引序列。import re, collections def tokenize(text): # 先按标点和空白切分再统一转小写 return re.findall(r[a-zA-Z], text.lower()) def build_vocab(tokens, min_freq2): counter collections.Counter(tokens) # 过滤低频词保留特殊符号 uniq [tok for tok, cnt in counter.items() if cnt min_freq] vocab [unk, pad, bos, eos] uniq return {tok: i for i, tok in enumerate(vocab)}几个实操要点。第一低频词必须过滤掉否则词表里全是只出现一次的噪声词转成索引后大部分位置都是长尾 id嵌入层学不动同时低频词统一映射到unk词表大小可控。第二特殊符号要放在词表开头并固定 idpad用于批内长度对齐bos和eos在生成任务里标记序列边界。第三处理中文时不能直接用空格切分要么按字切要么接一个分词工具两种方案的取舍是按字切词表小、无未知词但序列变长分词序列短但需要处理分词不一致。提示做批处理时长度对齐的填充方向会影响 RNN 的效果。左填充把pad放在前面配合取最后一个有效时刻的隐状态比右填充再手动索引要少写一堆代码也不容易错位。这个技巧在批大小变化频繁的实验里特别省事。6.3 语言模型与困惑度指标怎么读语言模型的目标是估计一个序列出现的概率把它拆成条件概率连乘。n-gram 模型是历史上最简单的做法用前 n-1 个词预测下一个词靠统计语料里的频次。它的最大问题是稀疏没见过的组合概率为零需要拉普拉斯平滑给每个计数加一个小量。评估语言模型常用困惑度定义是交叉熵的指数。它的直观含义是模型在每一步平均需要在多少个候选词之间犹豫所以数值越低越好。如果一个词表大小是 10000 的模型困惑度从 200 降到 100意味着平均候选数少了一半。看这个指标有个细节不同词表大小下困惑度没有可比性两个模型对比时必须用同一个词表。6.4 RNN、BPTT 与梯度裁剪循环神经网络在每个时刻共享同一组权重隐状态把历史信息压缩成一个固定长度的向量。这个设计让参数量与序列长度无关但也带来两个问题长程依赖衰减以及反向传播时梯度沿时间维连乘导致的梯度爆炸或消失。梯度爆炸的表现很直观损失突然变成 NaN 或者数值跳到极大。解决办法是梯度裁剪把梯度的范数限制在一个阈值内常见的阈值是 1 到 5。这个操作实现起来很简单但要注意裁剪应该在参数更新之前、所有梯度都算完之后统一做不要在每一层单独裁。梯度消失则要靠结构改进LSTM 和 GRU 用门控机制给梯度提供一条更直接的传播路径能记住更长的依赖。我在长度为 100 的序列上做过对比普通 RNN 在 20 步之后基本学不到依赖LSTM 能撑到 60 步以上。7. 常见报错与排查清单这段内容踩过的坑7.1 十二个高频问题速查表现象最可能的原因排查动作解决办法损失变成 NaN学习率过大或数值溢出打印第一步的损失值降学习率、加梯度裁剪、加 BN形状不匹配报错通道数或特征图尺寸算错逐层打印输出形状用公式重算或加自适应池化显存不够批大小或特征图过大打印每层激活占用减批大小、用梯度累积、混合精度训练损失降验证不降过拟合对比两条曲线加增广、Dropout、权重衰减训练损失也不降欠拟合或学习率太小看损失是否几乎不动加容量、提学习率、检查标签验证结果波动大忘了切 eval 模式检查 BN 和 Dropout 状态推理前调用 model.eval()结果无法复现随机种子未固定检查各处随机源固定种子并开启确定性模式数据加载是瓶颈单进程读取看 GPU 利用率提高 num_workers 和预取转置卷积尺寸差 1填充与输出尺寸公式混用手算一遍输出尺寸用转置卷积的输出尺寸公式检测框整体偏移坐标系或归一化不统一可视化原始框内部统一坐标约定锚框负样本过多锚框尺寸不匹配数据统计真实框宽高分布按分布重置锚框尺寸微调效果差冻结层与 BN 状态冲突检查哪些层在训练同步冻结 BN 到 eval这张表是我从笔记里出现频率最高的报错中筛出来的。用的时候建议从现象列找到对应行先做排查动作这一列不要直接跳到解决办法否则很容易改错地方把一个好配置改坏。7.2 我固定执行的四步排查动作第一先打印形状。任何形状相关的报错都用一个循环把每层的输入输出形状打出来这一步能解决一半以上的问题。我通常会在模型的前向函数里插一句形状打印用一个小批量跑一遍确认无误后再关掉。第二用一个极小数据集做溢出测试。取 8 张图或者 8 条序列关掉所有随机性看模型能不能在这个小样本上把损失降到接近零。如果连过拟合都做不到说明模型或损失函数有结构性 bug这时候去调参是浪费时间。第三把学习率调成 1e-4 再跑十轮。学习率过大是导致 NaN 和震荡的头号原因先用一个安全的值确认整条链路是通的再逐步往上调。这个顺序不要反。第四检查数据。把输入张量反归一化后存成图片或者打印成文本看一遍标签也看一遍。我遇到过标签整体偏移一位的低级错误损失曲线看起来只是学得慢查了两个小时才发现是数据加载的问题。7.3 这一段里被问得最多的几个概念题卷积层的参数量怎么算——记公式 c_out × c_in × k × k 加偏置别忘偏置。1×1 卷积有什么作用——通道混合、降维升维、增加非线性三个作用要知道。BN 在训练和推理时有什么不同——训练用当前批次统计量推理用移动平均。ResNet 解决的是什么问题——深层网络的退化问题不是过拟合也不是单纯的梯度消失。最大池化和平均池化的反传差异——前者梯度只回传给最大值位置后者平均分配。转置卷积是卷积的逆运算吗——不是只是上采样输出尺寸用另一条公式。这些问题本身不难难的是被追问细节时能不能答准确。比如问 BN接着一定会问移动平均怎么更新、momentum 的含义是什么、批太小怎么办这就从背概念变成考理解深度了。我的建议是每个概念都自己写一遍最小可运行代码验证一次比看十遍答案管用。8. 环境、算力与复现纪律8.1 本地环境的最小可用配置跑这段内容不需要顶级硬件。卷积基础的算子实验用 CPU 加 NumPy 就能做完LeNet 和 AlexNet 在 8G 显存的显卡上完全够用ResNet 级别建议 12G 以上目标检测和分割因为特征图大16G 比较舒服。环境上Python 3.9 以上、PyTorch 稳定版加 torchvision 就够了不需要额外装太多东西可视化用 matplotlib图像处理用 Pillow。CUDA 版本要跟 PyTorch 版本对齐这一条经常出问题最省事的办法是按官网给的命令装不要自己指定版本号。我建议单独建虚拟环境把这段内容涉及的书配套包和实验包分开装因为书里的工具包更新比较频繁容易出现接口变更。做法是建两个环境一个用来跟书里的代码一个装你自己项目的依赖。这样书里的代码升级时不会把你的项目搞坏。8.2 云上跑实验的取舍检测和分割任务在本地跑一轮可能要几个小时云上按小时计费会更划算。选实例的时候有三个指标按优先级显存大于算力算力大于 CPU 核数。显存不够直接跑不起来算力只是快慢问题。另外一定要确认镜像里 CUDA 和框架版本是匹配的自己装环境花的时间往往比跑实验还多。用云的一个纪律是先把小规模实验跑通再上大配置。我的习惯是本地或者小实例上用 1/10 的数据量跑通全流程确认代码没有结构性错误再换大实例跑完整训练。不然你会在按小时计费的机器上做本该在本地做的调试那是纯粹的浪费。另外记得把日志和模型检查点落到持久化存储很多平台的实例释放后临时盘上的东西就没了。8.3 复现实验的三条纪律第一条固定所有随机源。Python 的 random、NumPy 的 random、框架自己的随机数还有数据加载器的 shuffle 种子全部固定。GPU 上的某些算子是并行非确定性的需要额外开启确定性模式代价是速度下降一些做对比实验时必须开。第二条一次只改一个变量。加增广和换优化器一起改结果提升了你也不知道是谁的功劳。我在做消融时会把每个改动单独跑一遍记录在表格里这样最终写结论的时候有据可依。第三条把配置写进文件不要留在命令行里。学习率、批大小、增广强度、随机种子、数据版本全部落到一个配置文件里跟模型检查点一起存。我吃过亏两天前调出来的好结果回头想复现忘了当时的增广强度是多少只能重新试一遍浪费的时间比写配置文件多得多。最后分享一个我在这一段里最受益的习惯把每个集合的核心结论写成一句话贴在笔记最上面。比如第 34 集合是BN 训练用批次统计、推理用移动平均批太小会崩第 40 集合是锚框先统计真实框分布再设尺寸不要用默认值。这二十七句话连起来就是这一整段内容的骨架隔一段时间回看一遍比重新翻一遍代码快得多也更容易发现自己当时理解偏了的地方。
返回列表