PyTorch颜色识别实战:用HSV与轻量CNN让模型精度稳定95% 简介基于PyTorch的颜色识别项目代码涵盖数据预处理、模型构建、训练、检测与调试等完整流程适合作为课程设计、期末大作业或毕业设计参考也适合刚接触深度学习的初学者对照学习。压缩包内共14个文件包含9个Python脚本、1个已训练好的模型权重文件、环境依赖配置文件与说明文档脚本覆盖训练、检测、图像转换、调试验证等模块代码中附有详细注释整体大小约6.93MB部署轻便。主程序调用逻辑清晰功能完整且界面友好下载后简单配置环境即可运行能快速获得颜色识别结果。目前已有210人学习下载对于希望完成高分课设或入门PyTorch图像分类的读者这套代码可帮助理解颜色识别网络的设计思路并可在现有基础上进行二次开发与功能扩展实用价值较高。1. 颜色识别高分项目问题从来不在模型层数一张颜色识别模型通常坚持不到答辩的第三张测试图前两张是训练时的固定光源换到窗边自然光红色和橙色开始混深蓝和纯黑也开始混。这不是模型不够深而是这类 PyTorch 大作业最常见的丢分点——把颜色当成通道数值去硬拟合没有把颜色当成光照条件下的视觉分布。标题里的“高分项目”落地时靠的是三条主线用 HSV 把色相与亮度解耦、用 PyTorch 搭一个能学到色相分布的浅层分类网络、再补上评估闭环证明模型没有走捷径。按这个顺序做模型参数不到一万颜色识别精度也能稳定落在 95% 上下答辩时拿得出手的是可复现的指标不是“调了几百轮”。2. 颜色识别的数据管线先换掉 RGB再用 HSV 定标签2.1 为什么 RGB 通道不能直接当作网络输入颜色识别和一般图像分类最大的差别在于同一张红色桌布在日光灯下是 (200, 50, 50)在白炽灯下变成 (230, 80, 70)在阴影里可能降到 (100, 30, 30)。这三个 RGB 向量的欧氏距离远大于“红色”和“橙色”之间的典型距离网络要强行从这种光照扰动里学出稳定边界只能在训练集里硬背光源特征。所以直接用原始 RGB 做输入等价于把“识别颜色”这个任务改成了“识别光照环境”。HSV 的动机就是把亮度 V 和色相 H 分离算法上把“这是什么颜色”和“它有多亮”拆成两个变量。PyTorch 的torchvision.transforms不直接提供 RGB 到 HSV 的变换但你不需要在数据加载里手动转因为训练时给网络喂归一化 RGB 即可模型自己能学到去光照的表示。真正需要 HSV 的场景是数据准备阶段你要用它来快速确认每一类样本的区间分布以及排查错误标签。2.2 用 HSV 把颜色标签映射成可验证的区间这里有一个非常实用的中期检查手段用 OpenCV 读入所有训练图转成 HSV统计每个标签下 H 通道的直方图。正常情况下每个类别的 H 值应该集中在一段区间内如果直方图分散成两个峰说明这批数据里混了别的颜色或者标签本身就标错了。以下是一段直接在 Jupyter 里跑的检查脚本能在一分钟内定位标签噪声。import cv2 import numpy as np import glob from collections import defaultdict hists defaultdict(list) for path in glob.glob(data/train/*/*.jpg): # 目录结构: train/类别/图片 label path.split(/)[-2] img cv2.imread(path) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) h_channel hsv[:, :, 0] # OpenCV 里 H 范围是 0~180 hists[label].append(h_channel.flatten()) for label, h_list in hists.items(): all_h np.concatenate(h_list) p10, p90 np.percentile(all_h, [10, 90]) print(f{label}: H 通道 10%~90% 分位 {p10:.1f} ~ {p90:.1f})这段代码的逻辑是把每张图的色相通道拉平取每个类别的 10% 和 90% 分位数做成区间。分位数比均值更稳因为均值会被少量异常像素拉偏。参数上唯一的注意点是 OpenCV 的 H 范围是 0 到 180不是很多教程里写的 360转成matplotlib画图时同样要按 0 到 180 处理否则红色会被劈成两段。2.3 数据集目录组织与针对性数据增强颜色识别项目的数据量通常不大大作业场景下每个类别 50 到 200 张是常态。目录直接按ImageFolder的约定组织省掉手写 Dataset 的麻烦data/ train/ red/ img_001.jpg ... green/ img_001.jpg ... blue/ ... val/ red/ ...数据增强是这类项目最容易翻车的地方。最常见的一个误用是给颜色识别任务加上torchvision.transforms.ColorJitter随机改亮度、对比度、饱和度——你一边让它“学颜色不变性”一边随手把颜色标签改掉了等于给训练集注入错误标注。我一般的做法是只保留对空间位置不敏感的增强并且把分辨率控制在 32 或 48 像素因为颜色是全局信息不需要高分辨率。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((32, 32), interpolation2), # 2 双线性避免色块锯齿 transforms.RandomHorizontalFlip(p0.5), # 颜色与翻转无关可以加 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((32, 32), interpolation2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(data/train, transformtrain_tf) val_ds datasets.ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别:, train_ds.classes)这段加载管线有四个参数值得细说。Resize里的interpolation2指的是PIL.Image.BILINEAR颜色识别样本大多是纯色块或大片色域用最近邻插值会在边缘产生锯齿给卷积核添加无意义的纹理RandomHorizontalFlip是安全的因为镜像不改变像素颜色分布Normalize用的仍是 ImageNet 的均值和方差这里不需要为了颜色任务重新统计归一化只是为了数值稳定num_workers4在 Windows 下如果报多进程错误直接降到 0因为大作业数据量小多进程带来的加速有限反而可能引入排查成本。3. 用 PyTorch 搭一个浅层颜色分类网络把色相分布变成可训参数3.1 为什么颜色识别不优先上 ResNet现在很多模板代码都基于 pytorch 基础框架直接堆 ResNet18但颜色识别是一个典型的“低语义、高分布”任务判断依据是整张图的色调分布而不是物体轮廓、纹理层级或局部部件。ResNet 的强项恰恰在后三者。用 ResNet18 在这种任务上只会得到两个结果一是训练集上快速过拟合因为模型的容量远超任务需求开始去记忆每张训练图的光照细节二是答辩时被问“为什么用这么深的网络”时很难给出有说服力的选型理由。颜色识别真正需要的是“感受野覆盖全图但不深”的浅层网络。一个可以被答辩接受的设计是两层 3×3 卷积提取局部色块分布中间接 BatchNorm 稳定训练最后用全局平均池化把整张图的信息压缩成一个向量。全局平均池化在这里承担的是“把颜色统计量聚合起来”的角色比直接 Flatten 再甩给全连接层更能体现设计意图。3.2 可复现的 ColorNet 结构与参数量下面这份模型定义可以直接复制为一个model.py文件训练时 import 即可。import torch.nn as nn class ColorNet(nn.Module): def __init__(self, num_classes7): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 16, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(16), nn.ReLU(inplaceTrue), nn.Conv2d(16, 32, kernel_size3, padding1, biasFalse), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d(1) # 把 [B,32,H,W] 压成 [B,32,1,1] ) self.classifier nn.Sequential( nn.Dropout(0.3), nn.Linear(32, num_classes) ) def forward(self, x): feat self.features(x) # [B, 32, 1, 1] return self.classifier(feat.flatten(1))这份结构里藏着两个容易忽略的选型决定。第一输入分辨率只有 32×32两层 3×3 卷积的等效感受野是 5×5虽然单层感受野不大但颜色是全局频段信息经过全局平均池化后每个通道已经聚合了整张图的色相响应感受野不再是瓶颈。第二biasFalse配合BatchNorm2d因为 BN 层自带偏置项卷积层再加 bias 属于冗余参数这也是 ResNet 系列在BasicBlock里的标准做法。AdaptiveAvgPool2d(1)是对传统nn.Flatten()的替代。Flatten 会把 32 个通道的二维特征全部拉平再接全连接层会产生明显的参数量跳变平均池化先做通道内空间压缩最后只有一个 32 维向量进入分类器。这个设计决定了整个模型的可训练参数数量按每层展开可以精确估算层输出形状参数量约Conv2d(3,16,k3)[B,16,32,32]432BatchNorm2d(16)[B,16,32,32]32Conv2d(16,32,k3)[B,32,32,32]4608BatchNorm2d(32)[B,32,32,32]64AdaptiveAvgPool2d[B,32,1,1]0Linear(32,7)[B,7]231合计约 5367五万参数量不到即使在 CPU 上训练一个 epoch 也就几秒。对大作业而言这个规模意味着你可以大方地把完整训练日志和 loss 曲线放进报告里展示“模型轻量但指标稳定”的对比实验而不是只贴一张训练完成的截图。3.3 训练前的环境与数据形状检查在进入训练之前有一个经常在答辩现场翻车的环节环境版本不一致。不同 PyTorch 版本对torchvision.transforms的行为有细微差异比如旧版本里Resize的interpolation参数默认是最近邻新版本已经改成双线性。为了避免现场演示时出现“在家里能跑、在台上不能跑”的问题我一般会在训练脚本开头加一段设备与版本检查同时把输入张量的形状验证一并做掉。import torch print(PyTorch:, torch.__version__) print(CUDA 可用:, torch.cuda.is_available()) device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据形状冒烟测试跑一个 batch 的前向确认网络能通 sample_batch, _ next(iter(train_loader)) sample_batch sample_batch.to(device) model ColorNet(num_classeslen(train_ds.classes)).to(device) out model(sample_batch) print(前向输出形状:, out.shape) # 预期 [batch_size, num_classes]关于 PyTorch 安装这里多说一句先到 pytorch 官网复核当前稳定版的安装命令确认 CPU 版和 CUDA 版的差异训练脚本里用torch.cuda.is_available()做自适应。大作业项目经常被拷到别的机器上跑设备自适应代码能在演示时省掉“你装 CUDA 了吗”这类尴尬问答。输出形状检查的意义在于提前暴露类别数不匹配的问题——如果ImageFolder扫描目录时把某个空文件夹算成了一个类num_classes会比预期多前向不会报错但训练后混淆矩阵会多出一行永远为零的类别。4. 训练策略与控制变量让大作业的颜色识别分数稳定在 95% 以上4.1 固定随机种子与训练循环颜色识别项目拿到高分的前提是“可复现”。可复现不是指固定一个随机种子就算完而是让答辩老师在你的报告里能看到整套训练策略的可控性数据划分固定、权重初始化固定、评估集固定。下面的训练循环代码带了两种控制一是全局随机种子二是最优模型保存条件。import random import numpy as np import torch import torch.nn as nn import torch.optim as optim def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) model ColorNet(num_classeslen(train_ds.classes)).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) best_val_acc 0.0 for epoch in range(30): model.train() running_loss, correct, total 0.0, 0, 0 for x, y in train_loader: x, y x.to(device), y.to(device) optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() running_loss loss.item() * x.size(0) correct (model(x).argmax(1) y).sum().item() total y.size(0) train_acc correct / total val_acc evaluate(model, val_loader, device) # 复用下文的评估函数 scheduler.step() if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_color_net.pth) print(fepoch {epoch1:02d} | loss {running_loss/total:.4f} f| train_acc {train_acc:.4f} | val_acc {val_acc:.4f})几个参数的选择逻辑值得展开。weight_decay1e-4是浅层小模型的安全默认值它把权重的 L2 范数压住防止颜色这类低频特征被少数异常像素主导StepLR(step_size10, gamma0.5)表示每 10 个 epoch 学习率衰减一半在 30 epoch 的训练里正好做两次衰减适合数据量小、收敛快的场景比 CosineAnnealing 更容易解释。CrossEntropyLoss这里没有手工加权因为颜色分类数据通常是均衡的如果某个类别样本明显少于其他类优先考虑用WeightedRandomSampler做采样均衡而不是调 loss 权重——前者保留原始数据分布后者会改变梯度尺度排查起来更麻烦。4.2 超参数设定参考与调参边界给出一份可以直接照抄的超参基线并标注每项的可调范围和风险边界。参数推荐值可调范围调大后果调小后果输入分辨率32×3232~64训练变慢增益极低低于 24 时色块细节丢失batch_size3216~64需要同步调大 lr收敛变慢BN 统计不稳定学习率1e-35e-4~3e-3训练震荡val_acc 跳变收敛慢30 epoch 不够优化器AdamSGD动量Adam 足够换 SGD 要调 lrDropout0.30~0.5模型欠拟合出现过拟合迹象时调大训练轮数3020~50超过 30 基本无增益欠拟合val_acc 大概率低于 90%这里的核心边界是学习率。颜色识别任务的特征分布非常简单梯度方向高度一致学习率过大时 loss 曲线看起来在下降但混淆矩阵会显示某些类别被整体吞掉典型表现是红色和橙色互相混叠。如果训练曲线出现这种锯齿状 val_acc第一反应不应该是加网络深度而是把学习率降到 5e-4 重跑一次同时保留旧日志做对比。4.3 评估不是只看 accuracy混淆矩阵与失败样本单看 accuracy 最大的问题是无法回答“是哪两类在混”。颜色识别里最常见的混叠是相邻色相红和橙、蓝和紫、深蓝和黑。这些错误分布如果不用混淆矩阵展示只写一个 97% 的准确率答辩时被问到“哪个颜色最容易出错”就会卡壳。因此评估代码至少要输出三样东西总体准确率、每类 precision/recall/F1、混淆矩阵。from sklearn.metrics import confusion_matrix, classification_report import numpy as np def evaluate(model, loader, device): model.eval() y_true, y_pred [], [] with torch.no_grad(): for x, y in loader: x x.to(device) out model(x) y_pred out.argmax(1).cpu().tolist() y_true y.tolist() cm confusion_matrix(y_true, y_pred) report classification_report(y_true, y_pred, target_namestrain_ds.classes, digits3) val_acc np.mean(np.array(y_true) np.array(y_pred)) print(report) print(混淆矩阵:\n, cm) return val_acc # 保存失败样本用于报告中的错误分析 val_acc evaluate(model, val_loader, device)这段代码的重点在错误分析。跑完评估后把预测错误的样本单独拎出来按“真实颜色/预测颜色/置信度”整理成表格放进大作业报告里能直接支撑答辩中的两个高频问题错误集中在哪里看混淆矩阵的非对角线元素、为什么会错看失败样本的光照情况。参数上无需额外调整classification_report里的digits3记得要加否则默认输出两位小数精度变化很难在组间对比中看出来。5. 一个答辩能直接用的收尾推理函数、注意力验证与导出5.1 封装 predict 函数并输出 Top-3 置信度训练结束后模型还只是一个.pth权重文件。答辩演示时最忌讳现场写加载代码或者直接跑训练脚本。把推理逻辑封装成一个独立函数是让项目从“能跑”变成“好用”的关键一步。这个函数返回 Top-3 预测结果而不是只返回argmax因为颜色识别本身就存在相邻色相的固有歧义展示置信度分布比展示一个可能错误的单一结果更有说服力。from PIL import Image def predict_color(image_path, topk3): model.eval() img Image.open(image_path).convert(RGB) x val_tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob torch.softmax(model(x), dim1)[0] idx prob.argsort(descendingTrue)[:topk] return [(train_ds.classes[i], float(prob[i])) for i in idx] # 使用示例 for p in [demo/red_1.jpg, demo/orange_1.jpg]: print(p, -, predict_color(p))5.2 用热力图验证模型看的是“颜色区域”不是纹理这个验证技巧非常容易在答辩中加分。颜色识别模型如果学偏了可能是靠背景纹理或图像边缘来分类的。用第一个卷积层的输出做激活可视化能快速判断模型是否真的在响应彩色区域。具体做法给第一层卷积注册一个 forward hook取一个 batch 的输出特征图在通道维度上做平均得到一张空间热力图再放大到原图尺寸。def visualize_attention(image_path): model.eval() x val_tf(Image.open(image_path).convert(RGB)).unsqueeze(0).to(device) act {} def hook_fn(module, input, output): act[feat] output.detach() handle model.features[0].register_forward_hook(hook_fn) with torch.no_grad(): model(x) handle.remove() heat act[feat][0].mean(0).cpu() # [32, 32]通道维取均值 heat heat.unsqueeze(0).unsqueeze(0) heat torch.nn.functional.interpolate( heat, size(128, 128), modebilinear, align_cornersFalse ).squeeze() return heat.numpy()5.3 导出 ONNX 让演示环境脱离训练脚本最后一个小技巧是把模型导出成 ONNX 格式答辩现场只需要onnxruntime就能加载推理完全绕开 PyTorch 版本差异和训练代码依赖。导出代码只有几行但dynamic_axes参数直接影响后续推理的灵活性。dummy torch.randn(1, 3, 32, 32).to(device) torch.onnx.export( model, dummy, color_net.onnx, input_names[image], output_names[logits], dynamic_axes{image: {0: batch}, logits: {0: batch}} )dynamic_axes把 batch 维度标记为动态这样导出的模型既支持单张图片推理也支持一次传入多张图做批量测试不需要为演示场景单独维护两套推理代码。到这里这个 PyTorch 颜色识别项目就具备了完整的高分要素数据管线可复现、模型设计有依据、训练策略可解释、交付形态独立于训练环境。本文还有配套的精品资源点击获取