第T2周的任务是彩色图片分类这个名字看起来平平无奇但真正上手之后你就会发现它和前几周做的灰度图分类、手写数字识别完全不是一个量级。多了一个颜色通道模型要处理的参数直接翻倍输入数据的维度也多了一层网络结构、训练技巧、调参思路都得跟着调整。这一周的任务本质上是在帮你打通“从规则特征到自动特征”的最后一公里消化好了后面做目标检测、图像分割都有底气消化不好后面就容易卡在模型调不动、准确率上不去这些尴尬问题上。这篇文章我会直接用我实际跑通这个任务的思路来复盘包括我为什么选这个网络结构、每一层参数是怎么算出来的、训练时踩过的坑以及怎么把这套流程迁移到真实业务中。内容尽量保持新手也能看懂但不会省略关键的原理和计算过程。1. 这一周的任务到底在练什么1.1 从灰度到彩色多一个通道带来的连锁反应很多人在前几周练手时用的是灰度图也就是单通道的二维矩阵每个像素只有一个亮度值。到了彩色图片分类输入变成了三通道的RGB矩阵每个像素点有红、绿、蓝三个数值叠加在一起。这个变化看起来只是“从一个数变成三个数”但连锁反应很大。首先是数据维度变了。假设图片尺寸是32x32灰度图输入是(1, 32, 32)彩色图就是(3, 32, 32)。如果是全连接网络第一层就要把32x32x33072个像素值全部拉平作为特征。相比灰度图的1024个特征特征量翻了三倍网络参数量和训练数据需求量都跟着涨。这就意味着你不能完全照搬之前那个“输入层-隐藏层-输出层”的简单结构否则训练起来又慢又容易过拟合。其次是特征语义变了。灰度图里像素值表达的是亮度而彩色图片里不同通道的组合才能表达物体的颜色、纹理、边缘等视觉属性。比如一个红色苹果和一个绿色苹果灰度图上可能看起来差不多但彩色通道里的数值差异就非常明显。如果模型不会利用通道间的交互信息就很容易把不同颜色的同类型物体误判。这也是为什么彩色图片分类往往会搭配卷积神经网络来学。卷积操作天然能提取局部空间特征而且通过多卷积核可以在不同通道上做加权组合让模型自己去发现“什么颜色搭配代表什么类别”而不是靠人工去写颜色判断规则。1.2 分类目标与评估指标准确率之外还要看什么彩色图片分类的标准任务通常是给出一批带标签的彩色图像让模型预测每张图片属于哪个类别。常见的任务会涉及10个类别左右比如飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。当然不同教材或比赛会有差异但核心逻辑是一样的多分类互斥标签每张图只属于一个类别。初学者通常只看准确率这没问题但我个人建议这一周就养成看混淆矩阵的习惯。准确率只能告诉你“整体对不对”但不会告诉你“哪两个类别容易互相搞混”。我曾经训练一个10分类模型整体准确率看起来到了85%但混淆矩阵一看汽车和卡车经常分不清鸟和飞机也总混淆。原因并不复杂这两组物体在轮廓和纹理上有相似之处模型学到了共性但没学到决定性差异。如果只看准确率你根本不知道模型卡在哪也就不知道该往哪个方向调数据或调结构。另一个被低估的指标是各类别的召回率和精确率。彩色图片分类里有些类别样本多有些类别样本少模型很容易偏向样本多的类别。拿10分类任务举例如果“猫”类图片明显比“船”类多模型可能会把很多模棱两可的“船”判成“猫”整体准确率没掉太多但“船”的召回率会非常低。所以我建议每次训练完不光打印准确率还要打印每个类别的精确率、召回率和F1分数这样你才能真正掌握模型的短板。2. 模型设计为什么必须上卷积网络2.1 全连接网络为什么学不动彩色图片有朋友问过我既然彩色图片只是输入维度变大那我把全连接网络的隐藏层加宽、加深不也能分类吗理论上可以实际上很痛苦。我拿32x32x3的输入做了一个简单实验第一层隐藏层设置512个神经元光这一层的参数量就是3072x512512约157万。再往下加两层总参数直接冲到千万级。在小数据集上这样一个网络基本是“记题”而不是“解题”训练集准确率能到98%验证集只有55%。更本质的问题在于全连接网络缺乏对空间结构的利用。图片中相邻像素组成边缘边缘组成纹理纹理组成物体局部这是有层级关系的。全连接网络把每个像素当作独立特征完全没有局部性概念等于让模型从一堆乱序数字里硬找规律训练难度极高。而卷积网络通过滑窗扫描天然只关注局部邻域再接池化层压缩尺寸层层递进提取更大范围的特征这种结构性归纳偏置非常契合图像数据。彩色图片和卷积网络搭配还有一个关键优势卷积核可以跨通道操作。比如一个3x3卷积核作用于三通道输入时实际核尺寸是3x3x3它会把同一位置上的RGB三个值加权求和这等于自动学习颜色组合特征。如果是灰度图这个能力就用不上。2.2 一个精简CNN的每层算力账本很多教程喜欢直接丢给你一段现成的CNN代码但对每一层输出尺寸怎么变化、参数量多少讲得很少。我第一次看的时候也是一脸懵后来自己把每一层的手算账列出来才真正记住卷积、池化、全连接之间的衔接关系。以输入形状为(3, 32, 32)举例一个精简但足够完成10分类的CNN可以这样设计第一层Conv2d(3, 32, kernel_size3, padding1)输出的特征图尺寸仍然是32x32通道数变32。这里的padding1是为了保持尺寸不变计算公式是(32 - 3 21)/1 1 32。这一层参数量大约是33332 32 896加上偏置参数约896个参数。第二层Conv2d(32, 32, kernel_size3, padding1)输出依然是32x32x32。参数量是3332*32 32 9248。这一层的作用是堆叠感受野让后面的卷积看到更大的局部区域。第三层MaxPool2d(kernel_size2)输出变成(32, 16, 16)。池化没有可训练参数只是把每个2x2窗口的最大值保留下来降低特征图分辨率同时保留相对重要的激活信息。第四层Conv2d(32, 64, kernel_size3, padding1)输出变成(64, 16, 16)参数量是3332*64 64 18496。第五层Conv2d(64, 64, kernel_size3, padding1)输出为(64, 16, 16)参数量3364*64 64 36928。第六层MaxPool2d(kernel_size2)输出变成(64, 8, 8)。到这里模型已经把原始图像压缩成了64张8x8的“特征图”。第七层将特征图拉平得到6488 4096个特征输入到一个全连接层比如输出128维再经过ReLU和Dropout最后是10维输出接Softmax或直接用CrossEntropyLoss。我把这个结构在10分类彩色图片任务上跑过测试集准确率稳定在80%到85%之间训练时间也不长。当然这只是入门级模型想刷高准确率可以再加BatchNorm、换成更深的残差结构但这一周的关键是搞清楚每一层发生了什么结构不必过于复杂。3. 动手实现从数据加载到训练收敛3.1 数据加载与预处理细节这一周的任务里数据加载比往常更需要细心因为彩色图片的处理流程比灰度图多几个环节。第一步是确认数据的存储方式。如果图片是文件夹按类别存放的可以用很多框架自带的ImageFolder工具自动读取但要注意类别排序规则尤其是在Windows环境下不同系统可能对文件名的排序方式不一样导致标签顺序和预期不一致。第二步是归一化。彩色图片的像素值范围是0到255如果不做归一化直接喂给网络大的数值容易让梯度变化过于剧烈训练不稳定。常见做法是把像素值除以255让范围落在0到1之间。但很多老手会进一步用数据集的均值和标准差做标准化也就是对每个通道减去均值、除以标准差。这样做的原因是彩色图片的RGB三个通道分布并不一致比如蓝天背景多的图片蓝色通道均值就偏高。如果不做标准化模型会下意识更关注均值高的通道这不是我们希望看到的。第三步是尺寸调整。不同来源的彩色图片尺寸可能不一样而模型输入要求固定尺寸比如32x32或224x224。调整尺寸不是简单拉伸就完事了如果原图是长方形直接拉伸会让物体变形。更稳妥的方案是先等比缩放让短边达到目标尺寸再居中裁剪或者在缩放后做随机裁剪。这样既能统一尺寸又能让模型看到物体不同位置的形态附带一定的数据增强效果。3.2 模型定义与训练循环我把这一周的核心代码结构写在这里基于PyTorch框架但思路同样适合其他框架。代码不追求花哨重点是完整跑通训练流程。import torch import torch.nn as nn import torch.optim as optim class SimpleCNN(nn.Module): def __init__(self, num_classes10): super(SimpleCNN, self).__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 128), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x model SimpleCNN(num_classes10) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001)训练循环的部分我只说几个关键细节。模型要先设为训练模式也就是调用model.train()因为Dropout和BatchNorm在训练和推理阶段的行为不同。每个batch的数据要记得调用inputs, labels data将数据移到GPU或CPU上。前向传播得到输出后计算损失然后反向传播这里最容易被新手忽略的是每次更新前都要调用optimizer.zero_grad()否则梯度会不断累积导致Loss不停震荡。我第一次跑的时候忘了这行看到的Loss曲线就像锯齿一样后来才意识到是梯度累积的问题。训练多少次合适我建议每个epoch结束后都在测试集上做一次评估打印准确率和Loss。不要等全部训练完才看结果。这样你可以实时判断模型是欠拟合、过拟合还是根本没学进去。3.3 数据增强用有限的图片扩大训练视野彩色图片分类任务经常会碰到训练集数量不够的情况。同样一个物体换个角度、换个光照、换个背景模型可能就不认了。数据增强是应对这种情况最有效的手段之一它通过对原始图片做一系列随机变换生成“看起来像新的”训练样本。常用的增强操作有随机水平翻转、随机裁剪、随机旋转、颜色抖动、亮度对比度调整等。我自己在这一周的任务里最常用的是随机水平翻转和随机裁剪。水平翻转相当于模型能看到物体的镜像版本尤其对对称性较强的物体比如汽车、船效果很好。随机裁剪则可以让模型关注物体局部特征而不是每次看到同样的完整构图。需要注意的是增强不能脱离任务语义。如果你的图片里有文字方向水平翻转就会把文字变成镜像导致语义错误。在一般物体分类任务里翻转问题不大但如果遇到车牌识别、文字识别这类方向敏感的图片就不能随机翻转。另外颜色抖动也要适度过度调整会产生很多看起来很怪异的图片反而干扰模型学习。我通常会这样组合增强策略先随机裁剪到目标尺寸再随机水平翻转最后做标准化。验证集不做增强只做尺寸调整和标准化这样才能保证评估结果反映真实泛化能力。4. 训练过程里最常见的四个坑4.1 验证集准确率上不去先检查标签对齐有一回我的模型训练了好几个epoch训练Loss一直在下降验证准确率始终徘徊在10%左右跟随机猜测差不多。我一度以为是网络结构出了问题反复调参都没用。后来无意中打开了几张训练样本才发现读取图片时把通道顺序搞反了G和B通道互换导致整张图片颜色完全错乱。但因为图片看起来还是“正常”的人眼不一定能立刻察觉模型却学到了错误的颜色特征验证集自然一塌糊涂。这类问题在彩色图片分类里很常见尤其是从不同平台下载的数据集图片格式可能是RGB也可能是BGR。许多图像处理库默认读出来是BGR而很多深度学习库默认期望RGB两者不一致时模型就会把红色和蓝色信息搞混。检查方法很简单在训练前随机打印几张图片和对应的标签用肉眼看一遍。如果发现颜色诡异第一件事就去排查通道顺序。4.2 Loss震荡不收敛学习率背了多少锅训练初期Loss不降或者震荡太剧烈很多人第一反应是调网络结构但我见过的案例里更多是学习率设置不对。学习率太大模型参数会在最优点附近反复横跳Loss曲线上下剧烈波动。学习率太小参数更新太慢Loss像蜗牛一样爬肉眼看起来仿佛不降。我一般会固定先用0.001的初始学习率跑几个epoch观察Loss下降趋势。如果Loss在前几个epoch内基本没降可以尝试调大到0.003或0.01。如果Loss直接爆掉变成NaN或者像过山车一样就先调回0.0003。这里有一个经验Adam优化器本身对学习率有一定容忍度但依然遵循“初始学习率不宜过大”的原则。另外还推荐使用学习率衰减或余弦退火策略。简单做法是每隔若干个epoch把学习率乘以0.1这样在训练后期可以用更小的步子去微调参数让模型更稳定地收敛。这一招在彩色图片分类上非常管用前期大步狂奔快速接近最优点后期小步慢走精确定位。4.3 过拟合的明显信号与刹车方法彩色图片分类和灰度图分类相比过拟合的表现来得更快因为模型参数多、数据维度高。过拟合最典型的现象是训练集准确率快速趋近于100%而验证集准确率突然不再上升甚至开始下降。我最早犯的错是看到训练准确率一直涨就很兴奋直到验证准确率掉了才反应过来。刹车方法首先考虑增加Dropout比例。很多入门模型Dropout默认是0.5但如果你的数据量特别少可以调到0.6甚至0.7。其次可以考虑权重衰减也就是在优化器中加weight_decay参数比如0.0001类似给参数加一点“约束税”防止参数变得过大。再有就是早停法我习惯在每个epoch后监控验证准确率如果连续多个epoch没有提升就保存当前最好的模型并停止训练。不要觉得早停很“菜”实际工程里它是最经济实用的防过拟合手段。4.4 显存不够时的实用策略很多初学者在跑彩色图片分类时会遇到显存不足的报错因为彩色图片的数据量是灰度图的好几倍一个批次稍微大一点就可能爆显存。我当时的做法是把batch_size从128降到64如果还不够就降到32。很多人担心batch_size变小会影响模型收敛但实际上在数据集不大时小batch反而有助于收敛到更平缓的极值点。另一个策略是降低输入图片分辨率。很多任务并不需要224x224这么大的输入32x32或64x64就足以区分大部分类别。分辨率调低后训练速度会快很多而且显存占用会显著下降。如果做迁移学习还可以选择冻结主干网络的前几层只训练后面分类层这样会大大减少需要保存的梯度信息也相当于降低了显存压力。5. 把彩色图片分类能力迁移到真实任务5.1 从标准数据集到自建数据集这一周练完后很多人会想拿自己收集的图片做分类但很快会发现标准数据集上跑得好不代表自建数据集也能跑得好。自建数据集的第一个坑是样本数量不均衡比如A类别有1000张B类别只有50张。这时候有两种常见思路一是收集更多数据二是对样本少的类别做更强的数据增强。第二个坑是背景干扰。标准数据集里的图片通常主体明确、背景简单而自建数据集的图片可能背景杂乱模型很容易学到背景特征而不是物体特征。比如你要分类“汽车”和“飞机”如果所有汽车图片都是城市背景所有飞机图片都是天空背景模型就会把天空当作“飞机”的特征。解决办法是尽量让各类别图片的背景多样化或者做人像/物体带分割但这比较复杂。作为第一步我建议采集数据时有意覆盖多种背景防止模型偷懒走捷径。第三个坑是标签噪声。自建数据集的标注完全依赖人工偶尔标错很正常。少量标签错误不影响大局但如果错误比例超过5%模型很难学出稳定边界。我的习惯是训练前抽测一批图片统计标注错误率训练完成后把预测错误且置信度高的样本打印出来往往能发现一批标注错误。5.2 二分类和多标签任务怎么改彩色图片分类的标准任务是单标签多分类但真实业务里还可能遇到两个变种二分类和多标签。二分类最简单把最后一层全连接的输出改为1损失函数换成BCEWithLogitsLoss输出通过Sigmoid映射到0到1之间大于0.5判为正类。其他结构基本不用改。多标签任务就需要独立处理每一个标签比如一张图片可能同时包含“猫”和“草地”那么模型最后一层有几个标签就输出几个节点每个节点都做Sigmoid而不是Softmax。损失函数同样是多个二分类损失的叠加也就是BCEWithLogitsLoss但考察指标要从准确率换成每个标签的F1分数。修改时有一点容易搞错多标签任务的数据标签不能做成整数索引而要改成多维0/1向量。比如10个类别里图片包含“猫”和“狗”那标签向量就是[0,1,1,0,0,0,0,0,0,0]。批处理时标签形状是(batch_size, num_classes)这样才能和模型输出形状对齐。这一点看着简单但我在代码里踩过好几次形状不匹配的报错每次都要回去重新看文档才想起来。这套彩色图片分类的基础流程迁移到真实场景时核心思路不变先把数据管好再让模型自动学特征最后根据任务形态调整输出层和损失函数。至于模型结构升级等你把这一周的基础结构完全吃透后再换也不迟当时我在跑一个工业现场的零件分类项目时就是用类似上面这个精简CNN起步的后来只在结构上加了一层BatchNorm把Dropout稍做调整就达到了项目要求。很多真实任务对准确率的期待并没有比赛那么高稳定的80%到90%已经能解决大量实际问题。彩色图片分类的难点从来不是某一个模型有多强而是你能不能把“数据-模型-训练-评估”这条链路完整地跑通并且知道每一环节出了问题时该从哪里下手。把这一周吃透后面不管是换更深的网络、换更大的数据集还是换业务场景你都会比那些只会调包的人多一份底气。
阅读完成 · 觉得有帮助?