ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于CNN和PyTorch的衣服颜色识别:从数据到部署完整指南

基于CNN和PyTorch的衣服颜色识别:从数据到部署完整指南

每到毕设选题季,总会有学生跑来问我,深度学习方向的题目选什么才不容易翻车。聊得多了我发现,大家真正担心的其实不是题目大小,而是两件事:能不能讲清楚实现逻辑,能不能跑出看得见的结果。用 Python 写一个基于 CNN 卷积神经网络的模型,去识别不同衣服颜色,恰好就是这样一个“上下限都很清晰”的题目——它不需要你堆复杂的网络结构,也不需要顶配显卡,但过拟合、数据不均衡、光照干扰、模型评估这些深度学习里的典型问题,它一个都不会少。这篇文章按我实际辅导学生完成这个项目的推进顺序,把完整技术路线、调参经验和踩坑记录整理出来。

这份记录适合几类人:拿它当毕业设计题目的同学、想用图像分类练手的机器学习初学者,以及需要快速交付可视化演示的开发同学。文中的代码基于 Python 和 PyTorch 实现,网络主体是 CNN 卷积神经网络,数据采用公开服装图库加自拍样本的混合方式,任务设定为根据衣服主色完成多分类。下面先从选题逻辑讲起,再拆数据、拆网络结构、拆训练过程,最后给出一个可以直接复现的演示方案。

1. 选题逻辑与项目边界:颜色识别到底难在哪

还没动手写代码的人,最容易把“识别衣服颜色”当成一个送分题。人眼看颜色确实是本能,红色就是红色,蓝色就是蓝色。可一旦把这件事交给机器学习模型,问题就变得不简单:数码相机拍出来的 RGB 值受环境光照影响非常大,同一件白色 T 恤,在暖黄灯光下拍出来偏橙,在阴天窗外光下拍出来偏蓝。不同面料的材质特性也会改变颜色表现,丝光棉和牛仔布对光线的反射方式完全不同。再加上衣服常常带花纹、拼接、渐变,想让模型识别出“主色”,本身就需要先对整幅图的颜色信息做归纳和空间压缩。

所以这个项目的核心难点不是“模型能否学会分类”,而是“颜色这个语义概念能否稳定地被学到”。在项目设计阶段,我会先把边界定清楚:输入为一件衣服的正面全身图或局部图,输出为颜色类别,类别从最常见的色系中选取,比如黑色、白色、灰色、红色、橙色、黄色、绿色、蓝色、紫色、粉色这 10 类。如果图片里有多件衣服或复杂背景,就先裁剪或更换样本。这种做法不是偷懒,而是让实验变量可控——论文里才能讲清楚模型到底学到了什么。

1.1 为什么说它是典型的“不难不简单”题目

从教学角度看,这个题目的难度梯度非常合理。基础部分是数据整理、模型训练、指标评估,进阶部分可以加入特征图可视化、光照鲁棒性分析、类别混淆归因,任何一部分都能写出有血有肉的章节,而不是空谈理论。相比人脸识别、目标检测这类依赖大规模数据和重型框架的题目,衣服颜色识别对硬件要求低,单卡训练一轮只要十几分钟,学生能反复实验,这比一次训练跑几小时甚至几天的题目友好太多了。

1.2 类别范围要先定死,别一上来搞几十类

我也见过想直接做二三十类细分的,把浅蓝、天蓝、藏青、深蓝全部拆开。我的建议是第一次迭代千万别这么干。类别越细,类间差异越小,模型越容易学会“亮度差异”而不是“颜色本质差异”。更合理的做法是先跑通大类流程,后续再逐步细分,同时把容易混淆的类别合并成一组。这也是很多公开颜色分类任务只分 8 到 12 个基础色系的原因:在控制任务难度的同时,也保证人眼能对最终结果做出合理评判。

2. 数据集与预处理:同一件衣服在不同光线里是两种颜色

数据问题是这个项目里最先出现的坑。公开的服装数据集通常带类别、季节、风格等标注,但不一定直接提供颜色标签。我的做法是混合策略:先从公开服装图库中筛选一批背景干净、衣服占比高的图片,按主色人工归类;再补拍一些不同灯光条件下的实物照片,模拟真实场景里的颜色偏移。两部分合起来,训练集约 12000 张,验证集约 2000 张,并尽量让各类别数量均衡。

筛选时有个原则特别容易被忽略:不要只挑“一眼就能看出颜色”的图。如果所有训练图都在棚拍白光下拍的,模型学到的是很窄的颜色映射,部署到自然光环境会大幅退化。因此我会故意保留部分暖光、逆光、阴影样本,甚至单独做一个“光照扰动子集”放到验证集里检验鲁棒性。否则你训练出的模型可能只是背下了考试环境的答案,换个场景就露馅。

2.1 标注规则和“多色样本”怎么处理

标注策略我们采用两阶段规则:先看衣服整体,如果超过 60% 的面积属于同一色系,就按主色标注;如果不满足,就标成“多色”并从训练集中剔除。这条规则看上去粗暴,但对训练很有价值。因为 CNN 最终输出的是全图特征,如果一张图里又红又蓝,标注成红色会让模型产生严重的语义冲突。把多色样本剔除,比强行让模型学会“挑重点颜色”要安全很多,收敛速度也更快。

如果你不想人工筛,也可以用程序辅助筛选:先计算图片在 HSV 空间的色相直方图,如果主色相区间占比达不到 60%,就自动归入待人工复核队列。当时我写了个脚本批量统计直方图,筛掉了一大批拼接款卫衣和条纹 T 恤,效果立竿见影,比肉眼逐张看效率高出很多。

2.2 颜色空间转换:为什么我建议从 RGB 切到 HSV

预处理阶段,除了常规的 resize 到 224x224,最重要的一个技巧是颜色空间的转换。RGB 三个通道耦合性强,对亮度变化特别敏感;HSV 颜色空间把色相(H)、饱和度(S)、明度(V)拆开,其中 H 通道更接近人对颜色的感知。实际训练时,我会把 RGB 转 HSV,再把三个通道归一化后作为网络输入,让网络自行学习三者的权重。实验显示,仅这一项改动就能让深色类别的稳定识别有肉眼可见的改善,模型终于不用被 RGB 通道的强耦合牵着走了。

import cv2 import numpy as np import torch def rgb_to_hsv_tensor(pil_img): rgb = np.array(pil_img.convert("RGB")) hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV).astype(np.float32) hsv = hsv / 255.0 # 形状从 (H, W, 3) 转为 (3, H, W),方便送入网络 tensor = torch.from_numpy(hsv).permute(2, 0, 1) return tensor

这里要特别提醒:通用图像增强手段在这个任务里必须慎用。随机亮度、随机对比度、颜色抖动在一般分类任务里很好用,但在颜色识别任务里会直接制造标签冲突。你调亮一张红色卫衣 30%,它还是红色;可你把一张淡蓝色衬衫调成高饱和、高亮度,它在视觉上就可能接近紫色了。标签没变,特征却变了,模型自然学出一套混乱的映射。我实际训练时只保留水平翻转、随机缩放裁剪和极小范围的明度扰动,色相通道基本不动。

3. 卷积网络结构调整:小数据集下到底该加深还是该加宽

网络结构选择上,很多人的第一反应是拿预训练模型微调。但如果这是毕业设计,我通常建议至少自己搭一版 CNN 跑通,再用预训练模型做对照。原因不是预训练模型不好,而是全程黑盒微调的话,答辩时很难讲清楚每一层在干什么。自己搭的网络虽然精度可能低几个点,但可控性强、可解释性好,参数量和计算量都能直接算出来写成公式。真到了项目后期想提精度,再引入迁移学习也不迟。

针对这个任务,我最终选用的结构是:4 个卷积块,每块包含两层 3x3 卷积、一个批归一化、一个 ReLU 激活和一个最大池化;最后接全局平均池化和两层全连接分类头。通道数从 32 起步,每个块翻倍,最终到 256。这个思路类似小型 VGG,但通道数更克制,整体参数量在 1200 万上下,用一块 6GB 显存的显卡训练完全够用。

import torch.nn as nn class ClothColorCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( self._make_block(3, 32, 2), self._make_block(32, 64, 2), self._make_block(64, 128, 2), self._make_block(128, 256, 2), ) self.pool = nn.AdaptiveAvgPool2d(1) self.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(256, 128), nn.ReLU(inplace=True), nn.Linear(128, num_classes), ) def _make_block(self, in_ch, out_ch, num_conv): layers = [] for i in range(num_conv): layers.append(nn.Conv2d(in_ch if i == 0 else out_ch, out_ch, kernel_size=3, padding=1)) layers.append(nn.BatchNorm2d(out_ch)) layers.append(nn.ReLU(inplace=True)) layers.append(nn.MaxPool2d(kernel_size=2, stride=2)) return nn.Sequential(*layers) def forward(self, x): x = self.features(x) x = self.pool(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x

3.1 为什么用 3x3 堆叠而不是更大的卷积核

很多同学写代码时喜欢直接把卷积核改成 5x5 或 7x7,理由是“感受野更大”。但感受野大不等于效果好。两个 3x3 卷积堆叠,等价于一个 5x5 卷积的感受野,但参数量更少,而且中间多了一次非线性变换,表达能力反而更强。在小规模数据集上,这相当于用更少的参数获得同级别的能力,过拟合风险也会更小。如果你担心丢细节,可以在第二个卷积块后用膨胀卷积扩大感受野,但这步优化一般放到实验后期再做。

3.2 通道数、全局平均池化和 Dropout 的取舍

通道数设计同样有讲究,起步用 32 而不是 64 或 128,是为了控制模型容量在数据量允许的范围内。如果训练集只有一万多张图,通道数翻倍往往换来的是过拟合:训练集准确率冲到 99%,验证集却上不去。通道数太小又拟合不足,Loss 降不到理想值。以 32 起步,224x224 输入经过四层池化后特征图变成 14x14,空间分辨率依然保留,颜色的局部信息能顺利传导到全局特征。

全局平均池化在这里比直接展平更有优势。传统做法是把特征图展平再接全连接,参数量会爆炸;全局平均池化把每个通道压缩成一个数值,让最后一层卷积被迫学出更有区分度的空间特征,同时明显减小网络体积。全连接部分只保留一个隐藏层,中间加 Dropout 0.3,就足够应付 10 分类任务。结构再深,在这个数据规模下多半是吃力不讨好。

4. 训练调参实录:Loss 曲线不听话时的排查顺序

训练过程是整个项目里最容易心态崩掉的阶段。第一次跑实验时,训练 Loss 降到 0.3 左右就停住不动,验证集准确率卡在 84%。当时第一反应是模型不够强,差点去换更大的网络。后来冷静下来按顺序排查,问题出在数据输入链路——我们之前开了颜色抖动增强,幅度还比较大,等于给模型喂了互相矛盾的标签。关掉颜色抖动,只保留轻度明度扰动,同一结构下验证集准确率直接跳到 88.5%。这个教训后来反复讲给其他学生:遇到瓶颈,先查数据,再查超参,最后才怀疑模型结构。

4.1 优化器和学习率的搭配

训练时我先用 Adam,初始学习率 3e-4,batch size 64。Adam 收敛快、对学习率不敏感,适合快速验证想法。到了训练后期,我会切到带动量的 SGD,momentum 0.9,学习率降到 1e-3,并用余弦退火或阶梯式衰减。原因是 Adam 在接近局部最优时容易震荡,SGD 在精细搜索阶段更稳,最终精度常常高零点几个百分点。如果不想中途切优化器,也可以直接用 AdamW,weight decay 设 1e-4,过拟合压力会小一些。

下面这张表是我在不同配置下跑出来的直观感受,供参考:

配置适合场景优点需要注意
Adam,lr=3e-4快速验证收敛快,参数少后期易震荡
SGD + momentum,lr=1e-3后期精调稳定,精度更高收敛慢,对 lr 敏感
AdamW,wd=1e-4有轻微过拟合稳定,正则效果好超参数略多

4.2 早停、学习率调度和 Checkpoint

推荐一个非常实用的组合:用验证集 Loss 做监控,连续 5 个 epoch 不下降就把学习率乘 0.3,连续 10 个 epoch 不下降就停止训练并恢复最佳权重。这段逻辑实现不复杂,但效果很好,能自动适应不同数据集的学习节奏。另外,每个 epoch 结束后务必保存 Checkpoint,内容至少包括模型权重、优化器状态、当前 epoch 数、验证集指标,防止训练中途断掉后一切从头再来。

best_val_loss = float("inf") patience = 0 for epoch in range(max_epochs): train_loss = train_one_epoch() val_loss = validate() if val_loss < best_val_loss: best_val_loss = val_loss patience = 0 torch.save(model.state_dict(), "cloth_color_best.pth") else: patience += 1 if patience >= 10: break

4.3 显存不够?混合精度是你的救星

显存只有 4GB 的话,开启混合精度就能把 batch size 提上去,训练时间也能省一半左右。PyTorch 里通过 torch.amp 模块调用 FP16 前向计算和梯度缩放,不需要改模型结构。第一次跑混合精度时心里有负担,担心数值不稳,实测下来只要统一用 GradScaler 做反向传播,精度和 FP32 几乎没差别。如果你也是新手,建议先在小数据子集上验证稳定,再上全量数据。

5. 混淆矩阵与错误样本:黑/深蓝为什么总是认错

模型跑完不能只看 90% 的整体准确率就收工。把验证集预测结果做成混淆矩阵,逐类分析错误模式,这一步才是真正能区分“会调包”和“会做项目”的分水岭。我们最终版本的模型整体准确率约 90.7%,打开混淆矩阵后问题立刻暴露:黑色和深蓝色互认错误率最高,黑色样本里有 11% 被预测成深蓝色,深蓝色样本里有 8% 被预测成黑色;紫色和蓝色也常有混淆。这个现象并不难理解:黑色本来就不是某个固定波长,而是低明度下人眼对多种深色区的统一称呼,深蓝牛仔布在照片里的数值表现与纯黑差异极小。

5.1 按行归一化的查全率才是真实短板

把混淆矩阵按行归一化后,每一项才是该类别的“查全率”。下表是我整理出的重点错误流向,只看整体准确率永远看不到这些细节:

真实类别查全率主要误判去向
黑色89.2%深蓝色 11%
深蓝色91.0%黑色 8%
紫色87.5%蓝色 7%
橙色92.3%红色 5%
粉色93.1%紫色 4%

看到这些数字我反而踏实,因为这说明模型错误不是随机乱猜,而是有规律的。针对黑/深蓝混淆,我试了两种改进:一种是在 HSV 空间里给明度通道更高权重,让模型更重视亮度;另一种是调整类别定义,把深蓝样本中那些和黑色几乎无法区分的图片筛选掉。第一种提升有限,第二种效果更明显,但也说明一个事实:当类别边界连人都很难统一口径时,模型产生困惑很正常,调整标签定义的收益往往大于继续调参。

5.2 错误样本里的“合理错误”比指标更值得写进论文

逐张翻看错误样本也很有价值,尤其要关注那些模型“错得有理”的图。比如红色卫衣带白色拼接袖口,模型预测成了粉色;黄色衬衫在强光高光区几乎变白,模型预测成了白色。这些错误都不是模型笨,而是光照和纹理干扰了主色判断。我的处理办法是继续筛选多色拼接面积过大的样本,并补充带高光的训练图。两轮迭代后,整体准确率从 90.7% 升到 92.4%,黑/深蓝错分率明显下降。

这里我想强调:别被准确率绑架。答辩时与其报一个虚高的数字,不如展示你对错误样本的理解。如果能解释黑色为什么容易和深蓝混淆,并能给出成型的改进方案,这比把准确率从 92% 硬刷到 93% 有用得多。我也见过学生为了刷分偷偷删掉困难样本,这种做法一旦被追问就会穿帮,整个项目的可信度都会崩掉。

6. 从模型到答辩 Demo:用 Streamlit 快速封装一个可演示的颜色识别器

模型再准,如果只停留在训练脚本里,答辩现场也撑不起场面。我的习惯是让演示至少覆盖两件事:上传任意一张衣服图片,模型在几秒内给出颜色分类和置信度;能展示预处理过程和特征图,证明模型关注的是衣服区域。基于这个目标,用 Streamlit 做一个轻量页面,把所有推理逻辑封装成后端函数,前端只负责上传和结果展示,几分钟就能跑起来。

import streamlit as st import cv2 import numpy as np import torch from PIL import Image class_names = ["黑色", "白色", "灰色", "红色", "橙色", "黄色", "绿色", "蓝色", "紫色", "粉色"] @st.cache_resource def load_model(): model = ClothColorCNN(num_classes=10) model.load_state_dict(torch.load("cloth_color_best.pth", map_location="cpu")) model.eval() return model def preprocess(pil_img): pil_img = pil_img.resize((224, 224)) rgb = np.array(pil_img.convert("RGB")) hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV).astype(np.float32) / 255.0 tensor = torch.from_numpy(hsv).permute(2, 0, 1).unsqueeze(0) return tensor st.title("衣服颜色识别 Demo") uploaded = st.file_uploader("上传一张衣服图片", type=["jpg", "png", "jpeg"]) if uploaded is not None: img = Image.open(uploaded).convert("RGB") st.image(img, caption="原图", use_column_width=True) tensor = preprocess(img) with torch.no_grad(): prob = torch.softmax(load_model()(tensor), dim=1)[0] top_idx = int(prob.argmax()) st.write(f"预测结果:{class_names[top_idx]},置信度:{prob[top_idx]:.2%}")

6.1 两个不起眼但致命的细节

第一个是convert("RGB")不能省。很多手机图片带 EXIF 方向信息,直接用 PIL 打开再 Resize,方向可能错乱;先统一转 RGB 再进统一路径能避免很多奇怪问题。第二个更关键:推理时的输入预处理必须和训练时完全一致。如果你在训练里把 RGB 转成了 HSV,推理里却用 RGB 归一化,模型看到的分布就完全不一样,精度会掉一大截。上面代码就是统一走 HSV 路径的,这一点我在带学生的过程中被反复验证过。

6.2 特征图可视化是答辩加分项

想展示更“硬核”的内容,可以在模型 forward 时把 features 部分的输出单独存下来,从四层卷积块各取一张特征图,用 torchvision.utils.make_grid 拼成大图,再用 cv2.applyColorMap 上色。演示页面上并排显示原图、浅层特征图、深层特征图,就能直观讲清网络逐层提取边缘、颜色块、面料纹理和整体轮廓的过程。评审老师看到这种可视化,通常比看到一个准确率数字更愿意给好评。

部署上,本地演示用一个st.file_uploader就够用。如果要封装成接口,把推理函数直接包成 FastAPI 接口即可,前端框架随意。必须注意的是,模型文件、依赖版本、CPU 或 GPU 环境要提前锁定,用 requirements.txt 固定版本,别等答辩当天才发现依赖升级后行为变了。

其实整个项目做下来,我最深的体会是:衣服颜色识别看起来“太简单”,真做完才知道它不简单,但这种不简单恰恰是学习收益所在。它的边界清晰,数据可控,结构透明,每一步都能被合理解释。每次带完这类项目,我都会跟学生强调同一句话:你的目标不是做一个全世界最准的颜色分类器,而是用一条完整、可信的实验链路证明你理解从数据到模型再到评估的每一环,并能根据错误模式做出有效决策。按这个思路走下去,这题目在毕设中拿到亮眼成绩是很稳的。

如果你后面想继续扩展,我很推荐做粗细粒度结合的层级分类:第一层判断颜色大类,第二层针对易混淆的深色小类做二次判别,结构不复杂但很有可讲性,能作为论文的一个亮点。数据采集上也可以继续积累不同光源、不同场景、不同材质的样本,把颜色识别系统的鲁棒性越做越厚。还有一个小技巧是,每轮实验都随手记录配置和指标,整理成简短实验日志,写论文时你会发现这些记录比任何理论都经得起推敲。这些经验,等你有时间了可以慢慢在实践中验证。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进