面料图像5分类实战:迁移学习与PyTorch实现
简介面向深度学习和计算机视觉方向的开发者与学生这份数据集包含灯芯绒、棉麻、牛仔、皮质、轻纱五类基础面料的图像多分类样本共1900多张图片并按7:3划分为训练集与验证集所有图像已统一分辨率预处理。数据包共1970个文件其中1969张JPG图像对应五类面料样本另附1个JSON标签文件便于直接加载为模型训练数据整体7z压缩包约63.03MB体积适中。已有102人学习使用适合作为图像分类入门实践或纺织品识别项目的数据支撑。借助这些数据可训练CNN等分类模型学习从纹理、质感等视觉特征中判别面料类型也可用于电商自动识别、纺织品质量检测等场景的验证与调优。1. 面料5分类数据集为什么1900张图能撑起一个可落地的图像多分类模型一套“灯芯绒、棉麻、牛仔、皮质、轻纱图像多分类数据集”解决的是服装电商、纺织供应链和质检场景里很常见的痛点你手上有一批商品图或面料样品图想知道每一张图的材质到底对不对版。5分类看起来简单但面料识别比猫狗识别难因为颜色和背景干扰大真正的判别信息藏在纹路和质感里。1900多张数据、5个类别每类平均不到400张这个规模从头训练CNN基本没戏但配合ImageNet预训练模型做迁移学习20来个epoch就能跑到90%上下的验证准确率。这类数据集和“73”划分方式足够支撑一个分类Demo、一个自动化质检预研也能当论文里的baseline。适合谁想快速验证面料分类可行性、需要一份干净且可复现的训练验证划分、后面打算把模型部署到生产环境的工程师。关键是别只顾着数图片数量要把划分策略用对并且避开高纹理任务里那些看不见的坑。2. 1900张图做面料5分类数据划分的边界条件与可复现脚本2.1 先看清5个类别的标签体系和易混边界处理这类数据时我一般先把5个类别的文件夹命名定死按“英文名中文映射”管理避免后续写训练脚本时半天对不上标签中文名文件命名典型外观灯芯绒corduroy表面有纵向沟槽条纹反光随纹路变化棉麻linen织纹粗糙有颗粒感颜色多为米白、浅灰牛仔denim斜纹织法蓝靛色为主可能有水洗做旧皮质leather表面光滑或有毛孔纹路高光明显轻纱gauze半透明网眼结构能看到背景透光标注分歧主要出现在两个地方。一是棉麻和轻纱颜色都浅织法都有横纵交织感看小图非常容易标串标注规则里要写清楚“轻纱必须是半透明、能透出背景”。二是皮质反光强烈时毛孔纹理完全消失会被标成普通均匀材质。另外商品图里偶尔拍到拼接材质比如牛仔拼接皮质这类图要提前定规则常见做法是“主体区域占比超过60%的材质为准”否则标注员没法干活标签噪声会直接拖垮后面所有环节。每类平均约380张图对做图像多分类来说是一份小而完整的数据。要留意图片尺寸和来源电商主图、面料拍摄图、搜图凑来的图都在一个集合里时分辨率从300×300到2000×2000都有可能。这些差异要留给数据预处理去解决而不是指望模型自己适应。2.2 7:3划分为什么只做随机划分会翻车1900张图平均到5类每类约380张。如果直接写一句random.shuffle再按比例切片最坏情况下某一类可能分出去300张训练、80张验证另一类反过来验证集准确率波动会非常大模型调参时你根本分不清是参数问题还是划分问题。正确做法是用分层采样sklearn里的train_test_split加stratify参数就能做到按类别比例划分。比例固定为7:3后训练集约1330张、验证集约570张每个类内部都严格保持70%进训练、30%进验证。需要先明确一点这个7:3是“训练集:验证集”的划分最终准确率是在验证集上测的而不是独立的测试集。后面要出论文或者做严格横向对比建议再多切一份test集或者直接用5折交叉验证取平均对工程验证来说固定一套7:3划分能保证baseline可复现比反复换划分挑最好结果可靠得多。2.3 可复现的目录划分脚本stratify和seed一起锁死假设拿到的数据是“每个类别一个文件夹”的原始结构划分脚本我一般写成这样import os import shutil from sklearn.model_selection import train_test_split DATA_ROOT fabric_dataset # 原始数据根目录下面直接放5个类别文件夹 OUT_TRAIN fabric_split/train # 划分后训练集输出目录 OUT_VAL fabric_split/val # 划分后验证集输出目录 CATEGORIES [corduroy, linen, denim, leather, gauze] paths, labels [], [] for label, cat in enumerate(CATEGORIES): cat_dir os.path.join(DATA_ROOT, cat) imgs [os.path.join(cat_dir, f) for f in sorted(os.listdir(cat_dir)) if f.lower().endswith((.jpg, .jpeg, .png))] paths.extend(imgs) labels.extend([label] * len(imgs)) # 7:3 分层划分每类内部都保持 70% 训练、30% 验证 train_paths, val_paths, train_labels, val_labels train_test_split( paths, labels, test_size0.3, train_size0.7, stratifylabels, # 必须按类别分层不能只做随机切分 random_state42, # 固定种子保证任何机器上划分结果一致 ) for split, imgs in [(train, train_paths), (val, val_paths)]: for img_path in imgs: rel os.path.relpath(img_path, DATA_ROOT) cat rel.split(os.sep)[0] # 从路径里取出类别名 dest os.path.join( OUT_TRAIN if split train else OUT_VAL, cat, ) os.makedirs(dest, exist_okTrue) shutil.copy2(img_path, os.path.join(dest, os.path.basename(img_path))) # 打印每个子集的数量确认划分结果 for split in [train, val]: total 0 for cat in CATEGORIES: n len(os.listdir(os.path.join(OUT_TRAIN if split train else OUT_VAL, cat))) total n print(f{split}/{cat}: {n}) print(f{split} total: {total})这段逻辑很简单但有两个点必须锁死。第一是random_state42固定随机种子换机器、换系统都得到同一份划分第二是stratifylabels它保证每个类别里都是7:3而不是整体上7:3。train_size0.7会和test_size0.3自动配合两张量加起来不需要手动凑。复制用shutil.copy2而不是move保留原始文件夹不动后面想重新划分或对比抽样都有后悔药。有同事问我为什么不用torch.utils.data.random_split它的问题在于只对样本下标做切分不支持按类别比例分层小数据集上很容易出现前面说的偏斜。做面料分类这种每类只有几百张的任务一类的偏斜就是十几个百分点的验证准确率波动不值得冒这个险。3. 用预训练模型跑通面料5分类ResNet18到EfficientNet-B0的参数与命令3.1 为什么选ImageNet预训练纹理迁移的自然优势在1900张图的规模上从头训练CNN基本必过拟合。ImageNet预训练权重对纹理边缘、条纹、反光面、颗粒结构这些低级视觉特征已经建立了很好的表示面料识别需要的沟槽、斜纹、网眼、毛孔等中等层次语义都是在这些特征基础上组合出来的。迁移学习是这类小数据图像多分类的首选几乎不存在例外。模型选型上我一般分两步走模型显存占用训练速度预期收益ResNet18约1GB快适合跑通流程作为baseline够用ResNet50约2GB中等准确率高1~3个点EfficientNet-B0约1.2GB较慢参数少准确率接近ResNet50先用ResNet18把数据划分、训练、评估整条流水线跑通确认没有bug后再上更大的模型。不要一上来就堆大模型1900张图的数据量限制下ResNet50到EfficientNet-B0的差距不会太大反而是数据增强和训练策略的影响更明显。实际项目中我也试过直接在5分类上加ViT效果并不好小数据上Transformer过拟合很严重除非做大规模预训练微调否则不如老老实实用卷积网络。3.2 数据增强面料分类里增强不能太暴力面料是纹理主导的图像增强策略和普通物体分类不太一样。ColorJitter的亮度、饱和度调太猛会让皮质和牛仔的颜色失真RandomRotation角度太大灯芯绒的纵向纹路方向会变得不符合真实世界习惯RandomResizedCrop的scale如果取到0.2以下裁剪区域太小模型看到的是面料局部而非整体质感。但完全不增强又会在小数据上过拟合。我常用的增强组合是先Resize到256再RandomCrop成224加一个RandomHorizontalFlip。水平翻转对面料绝大多数类别安全垂直翻转不建议默认开启因为灯芯绒和牛仔的纹路方向有实际意义。判断增强力度是否合适的办法先不开增强跑一个短版本如果训练准确率很快逼近100%而验证准确率落后一大截说明过拟合这时候再逐步加上增强而不是一次性全开。3.3 最小可跑训练代码PyTorch、ImageFolder与自己踩过的参数用torchvision.datasets.ImageFolder可以省去写Dataset类的步骤目录结构直接就是标签结构import os import random import numpy as np import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms, models from torchvision.datasets import ImageFolder def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) set_seed(42) device torch.device(cuda if torch.cuda.is_available() else cpu) # 面料图像增强不能破坏纹理细节 train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), # 轻度裁剪保留足够大的面料区域 transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds ImageFolder(fabric_split/train, transformtrain_tf) val_ds ImageFolder(fabric_split/val, transformval_tf) print(class mapping:, train_ds.class_to_idx) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) # 用ImageNet预训练权重替换最后一层全连接为5类 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 5) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) epochs 25 best_acc 0.0 for epoch in range(epochs): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) acc correct / total print(fepoch {epoch 1} loss {running_loss / len(train_ds):.4f} val_acc {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_fabric_cls.pt) print(fbest acc: {best_acc:.4f})几个关键参数说明一下。batch_size32在单卡16G显存上跑ResNet18没有问题如果是8G显存就降到16。lr3e-4配合AdamW属于稳健起点换到SGD的话建议lr1e-2加上momentum0.9我自己用下来AdamW在迁移学习里收敛更稳。weight_decay1e-4对这个小数据集算保守过拟合明显时可以调到1e-3。25个epoch是经验值一般在第10到第15个epoch验证准确率就开始平了后面保存的是在全量验证集上效果最好的权重而不是最后一个epoch。训练时如果loss一点不降先检查学习率是不是被设成了1e-5这种过小的值如果CUDA out of memory优先降batch_size如果最后一层输出维度报错检查模型输出节点是不是等于5。这些是每次上手新数据集最容易翻车的三个地方。4. 面料5分类模型的避坑清单纹理相近、反光与标注噪声4.1 皮质反光被模型学成了“高光特征”现象皮质这一类别的验证准确率总是上不去可视化预测结果后更明显模型对任何带高光的物体都倾向于判成皮质。原因皮质的固有特征是表面光泽和毛孔纹路但商品图里的高光很多来自影棚闪光灯过曝的光斑完全没有皮革的毛孔信息模型学到的是“亮斑”而不是“皮革”。解决给皮质样本增加针对性增强例如在训练管线里对输入图像做随机的高光抑制用torchvision.transforms.functional.adjust_gamma适度压低过曝区域更根本的办法是补充自然光下拍摄的皮质图。如果应用场景是电商主图训练数据最好也以同场景的拍摄图为主背景和打光条件越接近线上落地越稳。4.2 棉麻和轻纱的混淆在混淆矩阵里一直消不掉现象验证集总体准确率卡在某个位置不动单独看混淆矩阵错误主要集中在这两类的互相误判上。原因轻纱的半透明、布料褶皱和浅色光照会让网络看到“淡色、有穿插纹理”的图而棉麻的孔隙在压缩后也接近轻纱的网格感。小尺寸输入下这两种材质的差异被彻底磨平。解决把这两类的训练样本重新过一遍标注确认“是否能透出背景”的标注一致性。轻纱样本如果大多是透明窗纱可以加大输入分辨率让网眼结构更清晰棉麻样本尽量选取平整面料的大图避免褶皱太多干扰原本明显的颗粒织纹。还有一个技巧单独给棉麻和轻纱各跑一次二分类看准确率能否到95%以上如果做不到说明原始标签本身就有大量错标要先返工再回来做5分类。4.3 尺寸不统一让纹理细节消失现象原始图有的300×300有的2000×2000统一Resize到224后材质细节完全没法看模型准确率比预期低很多。原因大图缩小后粗纹理和细纹理都变成一片均匀颜色小图放大后网眼变成色块。面料识别依靠的是单位面积里的纹理重复次数分辨率乱,模型就没法建立稳定的纹理尺度。解决划分数据后先统计所有图片的尺寸分布把特别小的图排除或做无损放大。训练时不要直接用Resize(224)一步到位建议先缩放到长边1024再裁剪或用RandomResizedCrop(scale(0.5, 1.0))让模型见过不同尺度的纹理。推理时保持和训练一致的输入尺寸不要为了提速盲目把输入降到160或128面料这类密集纹理任务对分辨率非常敏感。4.4 固定seed后跨机器还是复现不了现象在A机器上训练val_acc是0.91在B机器上用同一个random_state42重跑结果变成0.87。原因只固定了划分数据的random_state没有固定PyTorch的训练随机性。DataLoader的shuffle、模型初始化、cuDNN算法选择都有自己的随机源。解决在训练脚本开头执行前面写的set_seed()再加两行torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark Falsecudnn.benchmarkFalse会牺牲一点训练速度换取完全可复现的结果。如果项目不需要严格复现只求跑得快可以保留benchmarkTrue但调参时看到效果波动不要慌张先确认是不是随机性导致的。4.5 验证集准确率虚高数据划分前没做去重现象训练时验证集准确率98%看起来非常好一到真实场景测试只有80%怀疑人生。原因数据源里有大量重复或近似重复的图同一块面料的不同角度图散落在训练集和验证集里模型等于提前见过答案。面料类数据特别容易出现这种情况厂商经常给同一块面料拍好几张不同角度光照略变但纹理完全相同。解决划分前按文件MD5做一次去重完全相同的文件只保留一份近似重复图用感知哈希pHash按汉明距离去重阈值一般设在10以内。这一步在数据准备阶段做后面所有结果才可信。很多图像多分类项目的准确率虚高不是模型强是数据划分不干净。5. 进阶与应用混淆矩阵怎么读、Grad-CAM看纹理以及导出ONNX部署训练完先不要急着看准确率先把验证集的混淆矩阵和分类报告打出来这一步能看出模型真正的弱点from sklearn.metrics import confusion_matrix, classification_report all_preds, all_labels [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: outputs model(images.to(device)) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm) print(classification_report(all_labels, all_preds, target_namestrain_ds.classes))混淆矩阵里对角线越高越好非对角线的高数值就是在互相混淆的类别对。我刚做面料分类时有一次棉麻和轻纱互相判错占所有错误的60%后来发现是轻纱图片里混入了大量棉麻材质的面料返工标注后准确率直接跳了5个点。如果想让模型决策过程可解释可以用Grad-CAM看一下模型在皮制图片上到底关注什么区域。如果高光处激活强烈而皮革毛孔处没有激活基本可以判断模型学的是反光而不是皮质纹理回到上一章的坑里去补样本。这个验证动作花不了10分钟但对判断模型能不能上线非常有价值。部署前转成ONNX用小一点的推理框架跑model.eval() dummy torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy, fabric_cls.onnx, input_names[image], output_names[logits], dynamic_axes{image: {0: batch}, logits: {0: batch}}, )导出后可以用onnxruntime在CPU上推理单帧耗时几十毫秒放到边缘盒子或API服务里都够用。有一件事我每次做图像多分类都会提醒自己不要在训练集上抽几张好看的图就宣布效果达标必须保留一批真实场景里的暗光、过曝、褶皱、拼接样本把它们加入验证和后续训练否则线上翻车率会给你上一课。希望这个方向能帮你的面料分类项目少走几步弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →