从零构建计算机视觉知识体系:神经网络架构、PyTorch与目标检测实战
1. 从零构建计算机视觉知识体系:为什么大多数人卡在“只会调包”这一步
搞计算机视觉这几年,我见过太多人拿着YOLO的预训练权重跑通一个检测demo就觉得自己“入门了”,结果一问NMS的IoU阈值怎么选、Anchor Box为什么要聚类、特征金字塔到底解决了什么问题,就支支吾吾说不清楚。这个项目标题里提到的“神经网络架构、PyTorch、目标检测算法、Python编码者计算机视觉应用”,其实是一条非常完整的从底层到应用的链路,但绝大多数教程把它们拆成了互不相关的碎片——讲CNN的只讲卷积核怎么滑,讲PyTorch的只讲nn.Module怎么继承,讲目标检测的直接甩一个torchvision.models.detection的调用示例。这种学法,学完还是只会调包。
我自己带过几个刚入行的同学,发现一个很普遍的规律:能跑通代码和能改代码之间,隔着一整套对数据流、梯度流、特征流的理解。比如你拿到一个预训练的ResNet做迁移学习,什么时候冻结backbone、什么时候解冻、BatchNorm的track_running_stats在微调时该不该改,这些决策背后都是对网络架构和训练动态的理解,不是查文档能查出来的。这个系列我想做的事情,就是把这条链路上的关键节点一个个拆开,讲清楚每个设计选择背后的“为什么”,同时给出可以直接复现的PyTorch代码。
这篇文章适合谁看?如果你已经会用Python写基本的脚本,对NumPy的数组操作不陌生,知道什么是矩阵乘法,但面对一个完整的CV项目时不知道从哪下手——比如给你一批标注好的图片,让你训练一个目标检测器,你脑子里没有一个清晰的pipeline——那这篇内容就是为你准备的。我不会假设你有深度学习框架的使用经验,但我会假设你愿意动手敲代码,而不是只看不练。
整篇内容会围绕四个核心板块展开:神经网络架构的设计逻辑(重点是CNN和ResNet系列)、PyTorch的工程化使用(不只是nn.Module,还有数据加载、训练循环、混合精度、模型保存与加载的坑)、目标检测算法的演进脉络(从R-CNN到YOLO到DETR,重点讲清楚每个阶段解决了什么问题)、以及一个完整的Python编码实战项目(从数据准备到模型部署的完整流程)。每个板块我都会给出可运行的代码片段和参数选择的计算过程,确保你看完能直接上手改自己的项目。
2. 神经网络架构:从卷积核到残差连接的设计逻辑
2.1 卷积到底在“卷”什么:用一个小例子把特征图尺寸算清楚
很多人第一次学卷积的时候,被padding、stride、dilation、kernel_size这几个参数搞得头晕。我用一个最直观的方式帮你理清楚:假设你有一张32x32的单通道图片,用一个5x5的卷积核,stride=1,padding=0,那么输出特征图的尺寸是多少?公式是(H - K + 2P) / S + 1,代入就是(32 - 5 + 0) / 1 + 1 = 28。这个28x28就是卷积核在图片上“滑”了28次的结果。如果你把padding改成2,输出就变成(32 - 5 + 4) / 1 + 1 = 32,尺寸不变。这就是为什么很多网络设计里用padding = (kernel_size - 1) / 2来保持特征图尺寸不变——为了让网络可以做得更深而不至于特征图过早缩小。
但这里有一个容易被忽略的细节:卷积核的参数量是K * K * C_in * C_out。假设输入是256通道,输出也是256通道,用3x3的卷积核,参数量就是3 * 3 * 256 * 256 = 589,824。如果换成5x5,参数量直接变成5 * 5 * 256 * 256 = 1,638,400,翻了将近三倍。这就是为什么VGG之后大家都用3x3堆叠——两个3x3卷积的感受野等价于一个5x5,但参数量只有2 * 3 * 3 * C^2 = 18C^2,而5x5是25C^2,省了将近30%的参数,同时因为中间多了一层非线性激活,表达能力还更强。
我在实际项目里踩过一个坑:有一次做一个细粒度分类任务,输入图片分辨率很高(1024x1024),我直接套了一个标准的ResNet-50,结果第一层7x7 stride=2的卷积直接把特征图降到了512x512,再经过maxpool变成256x256,后面几层下来特征图太小,小目标的细节全丢了。后来改成第一层用3x3 stride=1的卷积,虽然计算量上去了,但精度提升了将近8个百分点。卷积核大小和stride的选择,本质上是在计算量和空间分辨率之间做权衡,没有绝对的对错,关键看你的任务对空间细节有多敏感。
2.2 批归一化与激活函数:为什么ReLU之后还要有BN
BatchNorm这个层,刚出来的时候很多人不理解为什么它有效。原始论文说是解决了“内部协变量偏移”,但这个解释后来被MIT的研究者质疑了。我自己的理解更偏向工程视角:BN本质上是在每个mini-batch上做了一次标准化,让每一层的输入分布保持稳定,从而允许你用更大的学习率,加快收敛。你可以做一个简单的实验:去掉BN层,用同样的学习率训练,loss曲线会震荡得很厉害,甚至发散;加上BN之后,loss下降平滑得多。
但BN有几个坑必须注意。第一,训练和推理阶段的行为不一样。训练时用的是当前batch的均值和方差,推理时用的是训练过程中累积的running_mean和running_var。如果你在推理时忘了调用model.eval(),BN层会继续用batch统计量,导致输出不稳定。第二,batch size太小的时候BN效果会变差。因为batch统计量本身噪声就大,如果batch size只有2或者4,均值和方差的估计非常不准。这种情况下可以考虑用GroupNorm或者LayerNorm替代。第三,迁移学习时BN层要不要冻结。如果你的目标域和源域差异很大(比如源域是自然图像,目标域是医学影像),冻结BN的running stats通常更好;如果差异不大,让BN继续更新往往能带来更好的精度。
激活函数这块,ReLU是默认选择,但它有一个“死亡ReLU”的问题:如果某个神经元的输入一直为负,梯度就一直是0,这个神经元就再也活不过来了。LeakyReLU和ELU就是为了解决这个问题。我在实践中发现,对于深层网络(超过50层),用ReLU配合BN通常就够了;对于GAN或者强化学习这种对梯度非常敏感的场景,LeakyReLU更稳。另外,GELU在Transformer架构里是标配,但在CNN里用得不多,除非你在做视觉Transformer相关的任务。
2.3 残差连接:为什么“跳连接”能让网络深到1000层
ResNet的核心思想其实一句话就能说清楚:让网络学习残差映射F(x) = H(x) - x,而不是直接学习H(x)。这样如果最优的映射就是恒等映射,网络只需要把F(x)学成0就行了,比直接学一个恒等映射容易得多。这个设计让梯度可以通过跳连接直接传回浅层,缓解了梯度消失的问题。
但这里有一个细节很多人没注意:残差块里的BN层位置很关键。原始ResNet v1是把BN放在卷积之后、ReLU之前,顺序是Conv -> BN -> ReLU。后来ResNet v2(也叫Pre-activation ResNet)改成了BN -> ReLU -> Conv,把BN和ReLU放到卷积前面。这个改动看起来很小,但效果很明显:v2的梯度流更干净,因为跳连接上没有任何非线性操作,梯度可以无阻碍地传回去。我在做超深网络(比如ResNet-152)的时候,v2的结构收敛更快,最终精度也略高一点。
还有一个工程上的经验:如果你要自己实现ResNet,注意下采样块(downsample block)的处理。当输入输出通道数不一致或者特征图尺寸减半时,跳连接上需要加一个1x1卷积来匹配维度。这个1x1卷积的stride要跟主分支的stride一致,否则特征图尺寸对不上,会报错。我见过不少人在这一步卡住,其实就是忘了对齐维度。
3. PyTorch工程化:从写模型到训练一个能用的模型
3.1 Dataset和DataLoader:数据加载的瓶颈往往在这里
很多人训练慢,第一反应是模型太大或者GPU不够好,但实际上数据加载经常是真正的瓶颈。PyTorch的DataLoader有几个参数直接影响吞吐量:num_workers、pin_memory、prefetch_factor。num_workers设成CPU核心数通常是个好的起点,但也不是越大越好——每个worker都会复制一份数据集对象,如果数据集很大(比如加载了所有图片到内存),内存会爆。我一般设成min(8, os.cpu_count()),然后根据GPU利用率调整。
pin_memory=True这个选项,当你的数据需要从CPU传到GPU时,它会使用锁页内存,传输速度更快。但如果你用的是IterableDataset或者数据已经在GPU上了,这个选项就没用。还有一个坑:自定义Dataset的__getitem__里不要做太重的预处理,比如实时做数据增强(随机裁剪、颜色抖动)是可以的,但不要在里面读大文件或者做复杂的计算。如果预处理很重,考虑提前把数据预处理成二进制格式(比如LMDB或者HDF5),训练时直接读,速度会快很多。
我做过一个对比实验:同样的模型和数据,用原始JPEG图片实时解码+增强,GPU利用率只有40%左右;换成提前解码好的二进制格式,GPU利用率直接拉到85%以上,训练时间缩短了将近一半。数据管道的优化,往往比换一个更大的模型带来的收益更直接。
3.2 训练循环的细节:学习率调度、梯度裁剪、混合精度
PyTorch的训练循环看起来很简单,但魔鬼在细节里。先说学习率调度。StepLR是最简单的,每过多少个epoch把学习率乘以一个gamma。但实践中我发现CosineAnnealingLR或者OneCycleLR往往效果更好,因为它们在训练后期把学习率降得很低,让模型能收敛到一个更平滑的极小值。OneCycleLR还有一个好处:它先 warmup 再退火,对初始学习率不敏感,适合你不太确定该设多大学习率的情况。
梯度裁剪是另一个容易被忽略的点。如果你的loss突然变成NaN,大概率是梯度爆炸了。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)这行代码能救命。但注意,裁剪的阈值需要根据模型和任务调整,设得太小会限制模型的表达能力,设得太大等于没裁。我一般从1.0开始试,如果训练不稳定就降到0.5,如果训练太慢就升到2.0。
混合精度训练(AMP)是这几年最实用的工程优化之一。torch.cuda.amp可以让你用FP16做前向和反向传播,同时用FP32维护一份主权重,既省显存又提速。但有几个坑:某些操作在FP16下会溢出,比如exp、log、大数乘法。PyTorch的GradScaler会自动处理梯度的缩放,但如果你自己写了自定义的loss或者层,需要确保它们支持FP16。另外,BN层在FP16下容易出问题,通常建议把BN保持在FP32。我实测下来,ResNet-50用AMP训练,显存占用减少约40%,速度提升约30%,精度损失几乎可以忽略。
3.3 模型保存与加载:state_dict之外你还应该知道的事
torch.save(model.state_dict(), path)是最常见的保存方式,但如果你只保存了state_dict,加载的时候必须重新定义模型结构,而且结构必须完全一致。如果模型结构变了(比如改了层名或者加了新层),加载就会报错。更稳妥的方式是同时保存模型结构和权重,但这样会依赖具体的类定义,跨项目迁移时可能出问题。
我自己的习惯是:训练时保存两个文件,一个是state_dict(用于继续训练),一个是TorchScript或者ONNX格式(用于部署)。TorchScript的好处是它把模型结构和权重打包在一起,不依赖Python环境,可以直接在C++里加载。ONNX的兼容性更好,几乎所有的推理引擎都支持,但转换过程中可能会遇到不支持的操作,需要自己写自定义算子。
还有一个坑:加载模型时map_location参数一定要设。如果你在GPU上保存的模型,在CPU上加载时不指定map_location='cpu',会报错。反过来,如果你在CPU上保存的模型要在GPU上加载,也需要指定map_location。这个细节看起来小,但在部署时经常卡住人。
4. 目标检测算法:从R-CNN到DETR的演进逻辑
4.1 两阶段检测器:R-CNN家族到底在解决什么问题
R-CNN的思路很直接:先用Selective Search生成一堆候选区域(region proposals),然后对每个区域用CNN提取特征,最后用SVM分类。但这个方法慢得离谱——一张图片要跑2000个候选区域,每个区域都要过一次CNN,根本没法实时。Fast R-CNN做了两个关键改进:一是用ROI Pooling把不同大小的候选区域统一到固定尺寸,二是把分类和回归放在同一个网络里联合训练。这样只需要对整张图片过一次CNN,然后在特征图上做ROI Pooling就行了,速度提升了十几倍。
Faster R-CNN的贡献是用RPN(Region Proposal Network)替代了Selective Search,把候选区域的生成也交给了神经网络。RPN本质上是在特征图的每个位置上放一组Anchor Box,然后预测每个Anchor是前景还是背景,以及相对于真实框的偏移量。这里的关键参数是Anchor的尺度和长宽比,通常用K-means对训练集的真实框做聚类来得到。我做过一个实验:在COCO数据集上,用聚类得到的Anchor比默认的9个Anchor(3种尺度x3种长宽比)的mAP高了将近3个百分点。Anchor的设计直接影响到RPN的召回率,进而影响整个检测器的性能。
但Faster R-CNN也有它的问题:ROI Pooling在量化坐标时会有精度损失,对小目标不友好。Mask R-CNN提出了ROI Align,用双线性插值代替了量化操作,解决了这个问题。如果你在做实例分割或者小目标检测,ROI Align几乎是必选项。
4.2 单阶段检测器:YOLO和SSD的取舍
YOLO把检测问题直接建模成回归问题:把图片分成SxS的网格,每个网格预测B个边界框和对应的置信度,以及C个类别的概率。这个设计让YOLO的速度极快,但早期版本对小目标和密集场景的检测效果不好。YOLOv3引入了FPN(Feature Pyramid Network)和多尺度预测,在三个不同的特征图尺度上分别预测,小目标的检测能力大幅提升。YOLOv4和v5在数据增强、损失函数、网络结构上做了大量工程优化,比如Mosaic增强、CIoU损失、CSPDarknet backbone,把速度和精度的平衡推到了一个新的高度。
SSD的思路和YOLO类似,但它在多个尺度的特征图上都设置了不同尺度和长宽比的默认框(default boxes),然后预测偏移量。SSD的一个关键设计是用卷积层直接预测检测结果,而不是全连接层,这让它可以处理任意尺寸的输入图片。但SSD的Anchor设置非常依赖人工经验,不同数据集上需要重新调整。
我在实际项目里选检测器的时候,通常会问三个问题:第一,你的场景对速度的要求是多少?如果是实时视频流(30FPS以上),YOLO系列是首选;如果是离线处理,Faster R-CNN或者DETR可能精度更高。第二,你的目标尺度分布是什么样的?如果小目标很多,FPN几乎是必须的,YOLOv3之后的版本都自带FPN。第三,你的标注数据有多少?如果数据量很小(几千张),从预训练模型微调是唯一可行的方案;如果数据量很大(几十万张),可以考虑从头训练。
4.3 DETR:用Transformer做检测到底香不香
DETR(Detection Transformer)是Facebook在2020年提出的,它把检测问题建模成一个集合预测问题,用Transformer的encoder-decoder结构直接输出一组检测框,不需要Anchor,也不需要NMS。这个设计非常优雅,但训练收敛很慢——原始论文需要500个epoch才能收敛,而Faster R-CNN通常12个epoch就够了。后来Deformable DETR通过可变形注意力机制把收敛速度提升到了50个epoch左右。
DETR的优势在于它没有Anchor和NMS这些手工设计的组件,pipeline非常干净。但它的劣势也很明显:对小目标的检测效果一般,因为Transformer的注意力是全局的,小目标在特征图上占的像素太少,注意力容易分散。另外,DETR的推理速度比YOLO慢不少,不适合实时场景。我个人的看法是:DETR适合那些对精度要求高、对速度不敏感、且目标尺度比较均匀的场景,比如文档版面分析或者医学影像检测。如果你要做的是自动驾驶或者视频监控,YOLO系列仍然是更务实的选择。
5. Python编码实战:从数据准备到模型部署的完整流程
5.1 数据准备:标注格式转换与数据增强策略
假设你拿到了一批标注数据,格式是Pascal VOC的XML文件,但你想用YOLO训练,需要转成YOLO的txt格式。转换的逻辑很简单:YOLO的标注是class_id x_center y_center width height,所有坐标都归一化到0到1之间。写一个脚本遍历所有XML文件,解析出bndbox的坐标,然后做归一化就行了。但这里有一个坑:VOC的坐标是xmin, ymin, xmax, ymax,而YOLO需要的是中心点和宽高,转换公式是x_center = (xmin + xmax) / 2 / image_width,width = (xmax - xmin) / image_width。如果图片有旋转或者裁剪,还需要考虑坐标变换。
数据增强方面,目标检测的增强比分类复杂得多,因为你不仅要变换图片,还要同步变换标注框。常用的增强包括:随机水平翻转、随机缩放、随机裁剪、颜色抖动、Mosaic(把四张图片拼成一张)。Mosaic增强是YOLOv4引入的,它让模型在训练时看到更多样的上下文,对小目标的检测效果提升明显。但Mosaic有一个副作用:它会让batch里的图片分布发生变化,BN的统计量可能不准。如果用了Mosaic,建议把BN的momentum调小一点,或者用SyncBN。
还有一个经验:验证集不要做增强,只做必要的resize和归一化。因为增强后的验证集不能真实反映模型在真实数据上的表现。我见过有人为了“提升”验证集精度,在验证集上也做随机裁剪,结果模型上线后效果一塌糊涂。
5.2 模型训练:损失函数的选择与调参
目标检测的损失函数通常由两部分组成:分类损失和回归损失。分类损失一般用交叉熵或者Focal Loss。Focal Loss是为了解决正负样本不平衡的问题——在单阶段检测器里,背景框的数量远远多于前景框,如果不加权,模型会倾向于把所有框都预测成背景。Focal Loss通过(1 - p_t)^gamma这个因子,让容易分类的样本权重降低,难分类的样本权重保持。gamma通常设成2,alpha设成0.25。
回归损失这块,从最早的Smooth L1,到后来的IoU Loss、GIoU Loss、DIoU Loss、CIoU Loss,演进的方向是让损失函数更好地反映检测框的质量。Smooth L1的问题在于它独立地优化四个坐标,没有考虑框的整体性。IoU Loss直接优化预测框和真实框的交并比,但当两个框不相交时,IoU为0,梯度消失。GIoU引入了最小闭包区域,解决了不相交时的梯度问题。DIoU进一步考虑了中心点距离,CIoU再加上长宽比的一致性。我实测下来,CIoU Loss在大多数场景下比Smooth L1的mAP高1到2个百分点,而且收敛更快。
学习率方面,目标检测通常用比分类更小的学习率,因为检测网络有多个分支,梯度尺度不一致。我一般用1e-4到1e-3之间的初始学习率,配合warmup(前500到1000个iteration线性增加学习率),然后用Cosine退火。Batch size如果允许的话,尽量大一点(8到16),因为BN需要足够的样本才能估计准统计量。如果显存不够,可以用梯度累积来模拟大batch。
5.3 模型评估与部署:mAP的计算与推理优化
mAP(mean Average Precision)是目标检测最常用的评估指标。计算过程是:对每个类别,把所有的预测框按置信度排序,然后计算在不同召回率下的精度,画出PR曲线,曲线下的面积就是AP。所有类别的AP取平均就是mAP。但这里有一个细节:IoU阈值的选择会影响mAP。COCO的mAP是IoU从0.5到0.95,每隔0.05计算一次,然后取平均,这样能更全面地评估模型在不同定位精度下的表现。Pascal VOC的mAP通常只算IoU=0.5。
部署的时候,推理优化是重点。第一,把模型转成ONNX或者TensorRT,推理速度通常能提升2到5倍。第二,用半精度(FP16)或者整型(INT8)量化,进一步压缩模型大小和加速。但量化会带来精度损失,需要做校准(calibration)。第三,如果输入图片分辨率很高,考虑用滑动窗口或者图像金字塔,但要注意这会增加推理时间。我做过一个对比:同样的YOLOv5s模型,PyTorch原生推理在V100上大概是10ms一张图,转成TensorRT FP16之后降到3ms,INT8之后降到1.5ms,但mAP掉了大概2个百分点。速度和精度的平衡,最终还是要看你的应用场景能容忍多大的精度损失。
6. 常见问题与排查技巧实录
6.1 训练不收敛或者loss震荡怎么办
这是最常见的问题,原因可能有很多。第一步,检查数据。把几张图片和对应的标注可视化出来,看看标注框有没有画错、类别有没有标错。我遇到过有人把类别ID从0开始和从1开始搞混了,导致模型怎么训都训不好。第二步,检查学习率。学习率太大loss会震荡甚至发散,太小收敛太慢。可以做一个学习率扫描:从很小的学习率开始,逐渐增大,观察loss的变化,找到一个使loss下降最快的值,然后从这个值的一半开始训练。第三步,检查BN层。如果batch size太小,BN的统计量不准,可以试试GroupNorm或者冻结BN。第四步,检查梯度。用torch.autograd.gradcheck或者直接打印梯度的norm,看看有没有梯度消失或者爆炸。
还有一个容易被忽略的点:数据预处理和模型预训练时的预处理不一致。比如你用了ImageNet预训练的权重,但你的输入没有做ImageNet的归一化(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),模型的输入分布和预训练时差很多,收敛会变慢甚至不收敛。这个坑我踩过好几次,后来养成了一个习惯:每次用预训练模型,第一件事就是确认预处理是否匹配。
6.2 显存不够用的几种解决方案
显存不够是训练CV模型时的家常便饭。最直接的方法是减小batch size,但batch size太小会影响BN的效果。第二种方法是梯度累积:用小的batch size做多次前向传播,把梯度累积起来,然后再做一次参数更新。这样等效于大batch,但显存占用不变。第三种方法是混合精度训练,用FP16做前向和反向,显存占用能减少30%到50%。第四种方法是梯度检查点(gradient checkpointing),它在前向传播时不保存中间激活值,反向传播时重新计算,用时间换空间。PyTorch的torch.utils.checkpoint可以很方便地实现。第五种方法是模型并行,把模型的不同层放到不同的GPU上,但这需要修改模型代码,比较麻烦。
我自己的经验是:优先用混合精度+梯度累积,这两个组合能解决90%的显存问题。如果还不够,再考虑梯度检查点。模型并行一般是最后的选择,因为通信开销大,而且调试困难。
6.3 模型在验证集上表现好但实际部署效果差
这个问题通常有几个原因。第一,数据分布不一致。训练集和验证集可能来自同一个分布,但实际部署时的数据分布不同。比如训练集是白天拍的图片,部署时是晚上拍的,光照条件差很多。第二,过拟合。模型在验证集上表现好,可能是因为验证集太小或者和训练集太相似。第三,预处理不一致。训练时用的resize方法、归一化参数,部署时可能忘了同步。第四,后处理参数不同。比如NMS的IoU阈值、置信度阈值,训练时和部署时可能设得不一样。
排查的方法:把部署时出错的样本拿回来,可视化模型的输出,看看是漏检、误检还是定位不准。如果是漏检,可能是置信度阈值设得太高;如果是误检,可能是NMS阈值设得太低;如果是定位不准,可能是Anchor设置和实际目标尺度不匹配。我一般会做一个错误分析表,把错误分成几类,统计每类的比例,然后有针对性地优化。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| loss震荡不收敛 | 学习率太大、数据标注错误、BN统计量不准 | 可视化数据、学习率扫描、打印梯度norm | 降低学习率、修正标注、换GroupNorm |
| 显存不足 | batch size太大、模型太大、未用AMP | 打印显存占用、逐步减小batch | 梯度累积、混合精度、梯度检查点 |
| 验证集好但部署差 | 数据分布不一致、过拟合、预处理不一致 | 对比训练和部署的预处理、可视化错误样本 | 增加数据多样性、正则化、统一预处理 |
| 小目标检测效果差 | 特征图分辨率太低、Anchor尺度不匹配 | 可视化小目标的特征图、统计Anchor匹配率 | 用FPN、调整Anchor尺度、提高输入分辨率 |
| 推理速度慢 | 模型太大、未优化、后处理耗时 | profile各阶段耗时 | 转ONNX/TensorRT、量化、优化NMS |
6.4 几个我踩过的坑和对应的技巧
第一个坑:忘了设model.eval()。训练完模型直接拿来做推理,BN层还在用batch统计量,Dropout还在随机丢弃,结果输出完全不对。这个坑我踩过不止一次,后来养成了习惯:推理前第一件事就是model.eval(),训练前第一件事就是model.train()。
第二个坑:数据加载的num_workers设得太大。有一次我把num_workers设成了32,结果内存直接爆了,因为每个worker都复制了一份数据集。后来改成8,问题解决。num_workers不是越大越好,要根据数据集大小和内存来定。
第三个坑:学习率调度器的step调用时机。PyTorch的StepLR是在每个epoch结束后调用scheduler.step(),但如果你用的是OneCycleLR,它是在每个iteration后调用。搞混了会导致学习率变化不符合预期。用之前一定要看清楚文档。
第四个坑:多GPU训练时BN的统计量。用DataParallel的时候,BN是在每个GPU上独立计算的,等效于减小了batch size。如果batch size本来就小,BN的效果会很差。解决方案是用DistributedDataParallel配合SyncBatchNorm,但配置起来比较麻烦。如果条件允许,尽量用单卡训练,或者用足够大的batch size。
第五个坑:模型保存时忘了保存优化器的状态。如果你只保存了model.state_dict(),想继续训练的时候优化器的动量信息丢了,loss可能会突然跳一下。如果要断点续训,一定要同时保存optimizer.state_dict()和scheduler.state_dict()。
7. 一些关于学习路径的个人建议
如果你刚开始学计算机视觉,我建议不要一上来就啃Transformer或者DETR这些新东西。先把CNN的基础打牢:手动实现一个简单的卷积层(不用nn.Conv2d,用unfold和矩阵乘法),手动实现一个ResNet block,手动写一个训练循环。这些看起来“重复造轮子”的练习,能让你对数据流和梯度流有肌肉记忆。等你对CNN的每一个操作都了然于胸了,再去看Transformer的注意力机制,你会发现很多概念是相通的。
PyTorch的学习,我建议从读源码开始。torchvision.models里的ResNet实现只有两百多行,但涵盖了卷积、BN、残差连接、下采样等所有核心概念。把它读懂读透,比看十篇教程都有用。然后自己从头实现一遍,不参考任何代码,看看能不能跑通。这个过程会很痛苦,但收获也最大。
目标检测这块,不要一上来就追求SOTA。先用YOLOv5或者Faster R-CNN跑通一个baseline,然后逐步调整:换backbone、加FPN、改损失函数、调Anchor。每改一个地方,记录下mAP的变化,慢慢你就知道每个组件的作用了。我见过太多人直接拿DETR或者Swin Transformer来训,结果因为数据量不够或者调参不当,效果还不如YOLOv5。
最后,保持动手的习惯。看再多的论文和教程,不自己写代码跑一遍,都是纸上谈兵。我自己的经验是:每学一个新概念,就写一个最小的可运行示例。比如学ROI Align,就写一个脚本,输入一个特征图和一个ROI,输出对齐后的特征,然后可视化出来看看对不对。这种“小步快跑”的学习方式,比一次性啃一个大项目要高效得多。