☰
基于ResNet18的五类面料分类实战:1900张数据集调参到92%精度
2026/10/11 8:26:42 网站建设 项目流程

简介:这份基础面料图像多分类数据集面向计算机视觉学习者与深度学习开发者,聚焦灯芯绒、棉麻、牛仔、皮质、轻纱五类常见面料的自动识别问题,可用于纺织品检测、电商材质识别、智能穿戴等场景的分类模型训练与教学实践。资源包共1970个文件,以1969张jpg图像为主体,另附1个json标注文件,压缩包约63.03MB,并按7:3比例预先划分训练集与验证集,图像分辨率已统一预处理,便于直接投入卷积神经网络等分类算法训练。目前已有102人学习下载。数据集覆盖五类面料在服饰、家居、工业等领域的典型纹理与外观差异,读者可据此完成从数据加载、特征提取到模型评估的完整流程,并借助统一尺寸与规范划分减少训练偏差,提升模型泛化能力与分类准确率,为后续面料识别应用打下扎实基础。

1. 面料五分类数据集:1900 张图怎么切、怎么训、怎么不翻车

做服装电商的视觉检索、面料识别、以图搜款,绕不开一个最脏最累的环节:数据。灯芯绒、棉麻、牛仔、皮质、轻纱这五类面料,视觉上互相纠缠——灯芯绒的竖条纹在低分辨率下和牛仔的斜纹撞脸,棉麻的粗糙肌理和皮质的高光反射在强光下都能骗过模型,轻纱的半透明边缘更是让分割和分类一起崩。手上这份「基础面料:灯芯绒、棉麻、牛仔、皮质、轻纱图像多分类数据集」给了一个很实在的起点:5 分类、1900 多张、按 7:3 划分好训练集和验证集。它解决的不是「有没有数据」的问题,而是「怎么用一份已经切好的数据,把五类面料的分类精度做到能上线」的问题。适合做服装电商视觉、面料质检、以图搜款召回排序的工程师,也适合刚入门想找一个真实多分类任务练手的人。下面按我实际跑这类数据集的顺序,把选型、加载、训练、调参和踩坑一次讲透。

2. 五类面料为什么难分:先看清类间距离再谈模型

2.1 灯芯绒、棉麻、牛仔、皮质、轻纱的视觉特征拆解

拿到一个 5 分类数据集,第一件事不是写模型,而是把每一类的判别特征想清楚,否则后面调参全是玄学。灯芯绒的核心特征是平行竖条纹,条纹间距和光照方向强相关,侧光下条纹阴影明显,正面平光下几乎退化成纯色块。棉麻的核心是随机交织的粗纤维纹理,高频信息多,但没有固定方向性。牛仔的典型特征是斜纹(右斜或左斜)加靛蓝染色,颜色分布比纹理更稳定,所以颜色直方图对牛仔的区分贡献往往大于纹理。皮质的关键是表面高光和不规则褶皱,高光位置随光源变化,这是它最容易和棉麻混淆的地方——两者都有粗糙感,但皮质有镜面反射,棉麻是漫反射。轻纱最特殊,半透明、边缘羽化、常和背景或其他面料叠在一起,单看局部纹理几乎没有判别力,必须依赖整体透明度和边缘梯度。

把这五类的特征列出来,你会发现一个残酷事实:灯芯绒 vs 牛仔靠纹理方向,棉麻 vs 皮质靠反射类型,轻纱靠全局透明度。没有任何单一特征能同时分开五类,所以模型必须同时学到纹理、颜色、反射和透明度四种线索。这也是为什么直接用 ImageNet 预训练模型微调通常能到 80% 出头,但想上 90% 必须做针对性处理。

2.2 1900 张、5 分类、7:3 划分意味着什么

1900 多张分 5 类,平均每类 380 张左右,7:3 划分后训练集约 1330 张、验证集约 570 张。这个量级属于「小样本多分类」的典型区间:够微调一个预训练 backbone,但不够从零训练;够做数据增强,但增强过头会引入伪特征。每类 380 张里,如果还有拍摄角度、光照、背景的分布差异,实际有效样本会更少。

这里有个容易被忽略的点:7:3 划分是数据集已经切好的,但你要确认它是随机划分还是按类别分层划分。如果原始划分是随机的,小类别可能在验证集里只剩几十张,指标波动会非常大。我一般会先统计验证集每类的数量,如果某类少于 80 张,就重新做分层划分。下面这段代码就是干这个的。

import os from collections import Counter # 假设目录结构为 dataset/train/类别名/图片 和 dataset/val/类别名/图片 def count_per_class(root): counts = {} for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): # 只统计常见图片格式,避免把 .DS_Store 之类算进去 n = len([f for f in os.listdir(cls_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png', '.bmp', '.webp'))]) counts[cls] = n return counts train_counts = count_per_class('dataset/train') val_counts = count_per_class('dataset/val') print('训练集每类数量:', train_counts) print('验证集每类数量:', val_counts) print('训练集总数:', sum(train_counts.values())) print('验证集总数:', sum(val_counts.values())) # 检查验证集是否有类别样本过少 for cls, n in val_counts.items(): if n < 80: print(f'警告: 验证集类别 {cls} 仅 {n} 张,指标可能不稳定')

这段代码的逻辑很直接:遍历 train 和 val 下的类别目录,统计每类图片数。关键参数是图片后缀过滤,很多人统计时把隐藏文件也算进去,导致数量对不上。跑完之后重点看验证集每类是否低于 80 张,低于就说明原始 7:3 划分可能没做分层,需要自己重切。重切时用sklearn.model_selection.train_test_split加stratify=labels即可,这里不展开。

提示:统计数量时一定要过滤非图片文件,我见过有人把 Thumbs.db 算成样本,训练时直接报错。

3. 从零跑通五分类:数据加载、增强与训练脚本

3.1 用 ImageFolder 还是自定义 Dataset

这份数据集是标准的「按类别分文件夹」结构,PyTorch 的ImageFolder可以直接用,省掉写 Dataset 的功夫。但ImageFolder有个坑:它按文件夹名的字母序分配标签,灯芯绒、棉麻、牛仔、皮质、轻纱如果是中文文件夹名,排序结果不可控,而且中文路径在部分环境会出编码问题。我的习惯是先把文件夹重命名成英文,再写一个映射表,这样标签可控、可读、可复现。

import os import shutil # 中文类别名到英文标签的映射,顺序固定,避免 ImageFolder 字母序带来的不确定性 cls_map = { '灯芯绒': 'corduroy', '棉麻': 'linen', '牛仔': 'denim', '皮质': 'leather', '轻纱': 'chiffon' } def rename_dirs(src_root, dst_root): for cn, en in cls_map.items(): src = os.path.join(src_root, cn) dst = os.path.join(dst_root, en) if os.path.isdir(src): os.makedirs(os.path.dirname(dst), exist_ok=True) if not os.path.exists(dst): shutil.copytree(src, dst) # 用复制保留原始数据,别直接 move print('重命名完成,类别顺序:', list(cls_map.values())) rename_dirs('dataset/train', 'dataset_en/train') rename_dirs('dataset/val', 'dataset_en/val')

逻辑说明:把中文类别目录复制成英文目录,保留原始数据不动,方便回溯。参数上cls_map的键必须和实际文件夹名完全一致,包括可能的空格。复制而不是移动,是因为后面做数据增强或重新划分时还要用原始数据。跑完后dataset_en/train下就是 corduroy、linen、denim、leather、chiffon 五个英文目录,ImageFolder会按这个字母序分配 0 到 4 的标签。

3.2 针对面料纹理的增强策略:哪些增强会帮倒忙

数据增强是小样本任务的关键,但面料分类的增强不能照搬通用配方。翻转、旋转、颜色抖动是常规操作,但有几个针对性的坑:灯芯绒的竖条纹如果做水平翻转,条纹方向不变(竖条纹翻转后还是竖),没问题;但如果做 90 度旋转,竖条纹变横条纹,模型会学到错误的方向特征。牛仔的斜纹同理,旋转会破坏斜纹方向。所以我的增强策略是:只做水平翻转、小角度旋转(±15 度以内)、轻微颜色抖动,不做垂直翻转和 90 度旋转。

from torchvision import transforms # 训练集增强:保守策略,保护纹理方向特征 train_tf = transforms.Compose([ transforms.Resize((256, 256)), # 统一尺寸,短边缩放到 256 transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 随机裁剪,scale 下限 0.7 避免裁掉太多纹理 transforms.RandomHorizontalFlip(p=0.5), # 水平翻转安全,竖条纹和斜纹方向不变 transforms.RandomRotation(15), # 小角度旋转,超过 15 度斜纹方向会乱 transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), # 轻微颜色抖动 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 验证集只做 resize 和归一化,不做任何随机增强 val_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

参数说明:RandomResizedCrop的scale=(0.7, 1.0)是关键,下限设 0.7 是为了避免裁到只剩一小块纯色区域,那样灯芯绒的条纹和棉麻的纹理都会丢失。RandomRotation(15)的 15 度是经验值,超过这个角度斜纹方向开始明显偏移。ColorJitter的 hue 只给 0.05,因为牛仔的靛蓝色是重要判别特征,hue 抖动太大会把牛仔抖成别的颜色。归一化用的是 ImageNet 的均值和方差,因为后面要用预训练模型。

3.3 用 ResNet18 微调的最小训练脚本

backbone 选 ResNet18 而不是 ResNet50,原因很实际:1900 张图、5 分类,ResNet50 参数量太大,容易过拟合,而且训练慢。ResNet18 在 ImageNet 上预训练后,微调这个量级的数据集足够。如果你有 GPU 且追求更高精度,可以换 EfficientNet-B0,但 ResNet18 是性价比最高的起点。

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, models # 加载数据集 train_ds = datasets.ImageFolder('dataset_en/train', transform=train_tf) val_ds = datasets.ImageFolder('dataset_en/val', transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) # 构建模型:ResNet18 预训练 + 替换最后的全连接层为 5 分类 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, 5) # 5 类输出 model = model.cuda() # 损失函数和优化器 criterion = nn.CrossEntropyLoss() # 只微调全连接层时 lr 可以大一点,微调整个网络时 lr 要小 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) best_acc = 0.0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.cuda(), labels.cuda() optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.cuda(), labels.cuda() out = model(imgs) pred = out.argmax(dim=1) correct += (pred == labels).sum().item() total += labels.size(0) acc = correct / total if acc > best_acc: best_acc = acc torch.save(model.state_dict(), 'best_resnet18.pth') print(f'Epoch {epoch+1}, Val Acc: {acc:.4f}, Best: {best_acc:.4f}')

逻辑说明:先加载数据集,ImageFolder自动按目录名分配标签。模型用预训练 ResNet18,把最后的fc层换成 5 输出。优化器用 AdamW,学习率 1e-4,这个值对微调整个网络比较稳;如果你只想微调全连接层,可以把 backbone 参数冻结,学习率调到 1e-3。CosineAnnealingLR让学习率余弦衰减,30 个 epoch 是这类小数据集的常见训练长度。每个 epoch 后在验证集上算准确率,保存最好的模型。跑完 30 个 epoch,正常情况验证准确率能到 85% 到 92% 之间,具体取决于数据质量和类别平衡度。

注意:num_workers=4在 Windows 上可能报错,改成 0 即可;在 Linux 上保持 4 能明显加快数据加载。

4. 调参与排错:让五分类精度从 85% 到 92%

4.1 学习率、batch size、冻结层数的组合实验

上面脚本跑出来 85% 左右是基线,想往上走要做组合实验。我一般固定三个变量:学习率(1e-3、1e-4、1e-5)、batch size(16、32、64)、冻结层数(冻结全部 backbone、冻结前两层、不冻结)。1900 张图的量级,batch size 32 最稳,16 会让训练变慢且梯度噪声大,64 在单卡上可能显存吃紧。学习率方面,不冻结 backbone 时 1e-4 最好,1e-3 容易震荡,1e-5 收敛太慢。冻结全部 backbone 只训 fc 层时,1e-3 反而合适,因为只有一层要学。

冻结层数的选择有个经验:如果五类面料在你的数据里视觉差异明显(比如灯芯绒和轻纱),冻结 backbone 只训 fc 就能到 88%;如果差异细微(棉麻和皮质),必须解冻后面几个 stage 一起微调。下面这段代码演示如何冻结部分层。

# 冻结 ResNet18 的前两个 stage(layer1 和 layer2),只微调 layer3、layer4 和 fc for name, param in model.named_parameters(): if name.startswith('layer1') or name.startswith('layer2') or name.startswith('conv1') or name.startswith('bn1'): param.requires_grad = False else: param.requires_grad = True # 冻结后重新构建优化器,只传需要梯度的参数 optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4, weight_decay=1e-4 )

逻辑说明:named_parameters()遍历所有参数,按名字前缀判断属于哪个 stage。conv1和bn1是浅层特征,冻结它们能保留通用边缘纹理检测能力。layer1和layer2是中层特征,冻结后减少参数量,降低过拟合。layer3、layer4和fc解冻,让模型学习面料特有的高层语义。优化器只传requires_grad=True的参数,否则 PyTorch 会对冻结参数也计算梯度,浪费显存。

4.2 混淆矩阵告诉你哪两类在互相骗

准确率是个笼统指标,真正有用的是混淆矩阵。五类面料里,棉麻和皮质、灯芯绒和牛仔是最容易互相误判的两对。跑完验证后打印混淆矩阵,如果发现棉麻被大量预测成皮质,说明模型没学到反射差异,这时候要么加偏振光预处理,要么在增强里加高斯模糊模拟漫反射。如果灯芯绒被预测成牛仔,说明纹理方向特征没学好,可以尝试加大输入分辨率到 320,让条纹更清晰。

from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.cuda() out = model(imgs) pred = out.argmax(dim=1).cpu().numpy() all_preds.extend(pred) all_labels.extend(labels.numpy()) # 打印混淆矩阵和分类报告 cm = confusion_matrix(all_labels, all_preds) print('混淆矩阵:') print(cm) print('\n分类报告:') print(classification_report(all_labels, all_preds, target_names=['corduroy', 'linen', 'denim', 'leather', 'chiffon']))

逻辑说明:收集整个验证集的预测和真实标签,用confusion_matrix算出 5x5 矩阵,对角线是正确预测,非对角线是误判。classification_report给出每类的 precision、recall、f1。重点看 recall 最低的那一类,以及它主要被误判成哪一类。比如 linen 的 recall 只有 0.7,且大部分被预测成 leather,那就针对这两类做分析。参数上target_names的顺序必须和标签 0 到 4 对应,也就是ImageFolder的字母序。

4.3 类别不平衡时用加权损失还是重采样

1900 张分 5 类,如果某类只有 200 张、另一类有 500 张,就是类别不平衡。这时候有两个选择:加权损失或重采样。加权损失给少数类更高的 loss 权重,实现简单;重采样让少数类在每个 batch 里出现更多次,但可能过拟合少数类。我的经验是,不平衡比例在 2:1 以内用加权损失,超过 2:1 用重采样加加权损失一起上。

# 根据训练集每类数量计算权重,数量越少权重越高 train_counts = [len(os.listdir(f'dataset_en/train/{c}')) for c in ['corduroy', 'linen', 'denim', 'leather', 'chiffon']] total = sum(train_counts) weights = [total / (5 * n) for n in train_counts] # 5 是类别数 class_weights = torch.tensor(weights, dtype=torch.float32).cuda() criterion = nn.CrossEntropyLoss(weight=class_weights)

逻辑说明:weights的计算公式是总数 / (类别数 * 该类数量),这样少数类的权重会大于 1,多数类小于 1。class_weights传给CrossEntropyLoss的weight参数,loss 计算时自动加权。注意权重要在 GPU 上,和模型设备一致。如果某类数量为 0,这个公式会除零,所以统计前要确保每类都有样本。

提示:加权损失会让模型偏向少数类,可能拉低多数类的 recall,调完权重后一定要看混淆矩阵确认没有矫枉过正。

5. 避坑与排查:五分类数据集最容易翻车的 5 个地方

5.1 验证集准确率虚高,上线就崩

现象:验证集准确率 95%,但拿新拍的灯芯绒图片去测,模型预测成牛仔。原因:训练集和验证集来自同一批拍摄,光照、背景、相机参数高度一致,模型学到了背景特征而不是面料特征。解决:做一次跨域验证,用手机随手拍几张五类面料,不控制光照和背景,看模型表现。如果掉得厉害,说明数据多样性不够,需要在增强里加随机背景替换或更强的颜色抖动。

5.2 轻纱类几乎全错,透明边缘被当成背景

现象:轻纱的 recall 只有 0.4,大部分被预测成棉麻或皮质。原因:轻纱半透明,边缘和背景融合,模型在 resize 和 crop 时把透明区域裁掉了,剩下的纹理和棉麻接近。解决:对轻纱类单独做处理,resize 时用 padding 而不是直接缩放,保留完整边缘;或者在数据加载时对轻纱类不做 RandomResizedCrop,改用 CenterCrop 加 padding。

5.3 训练 loss 震荡不收敛,学习率设大了

现象:训练 loss 在 1.5 到 2.5 之间反复跳,验证准确率不升。原因:学习率 1e-3 对微调整个 ResNet18 太大,梯度更新步长过大。解决:降到 1e-4 或 1e-5,加 warmup 让前几个 epoch 学习率从 1e-6 线性升到目标值。如果还震荡,检查 batch size 是否太小,16 以下梯度噪声会很明显。

5.4 中文路径导致 ImageFolder 读不到图片

现象:ImageFolder报FileNotFoundError或返回 0 张图片。原因:中文文件夹名在某些 Python 版本或操作系统上编码不一致,os.listdir返回的字节串和实际路径对不上。解决:按 3.1 节的脚本把中文目录复制成英文目录,全程用英文路径。如果必须用中文,确保 Python 3.6+ 且文件系统编码为 UTF-8。

5.5 保存的模型加载后预测结果全一样

现象:训练时验证准确率 90%,加载best_resnet18.pth后预测所有图片都是同一类。原因:保存时用了torch.save(model, path)保存整个模型,加载时环境不一致导致部分层初始化错误;或者保存的是state_dict但加载时模型结构没对齐。解决:统一用torch.save(model.state_dict(), path)保存,加载时先实例化相同结构的模型,再model.load_state_dict(torch.load(path))。加载后打印几个参数的均值确认不是初始值。

6. 把五分类推到 93% 的两个进阶技巧

第一个技巧是测试时增强(TTA)。训练完之后,对验证集每张图做多次增强(水平翻转、小角度旋转),分别预测后取平均。这个操作不增加训练成本,通常能涨 1 到 2 个点。实现上写一个循环,对每张图生成 5 个增强版本,模型输出 softmax 后平均,取 argmax。注意 TTA 的增强要和训练增强一致,别用训练时没见过的增强方式。

def tta_predict(model, img_tensor, n_aug=5): model.eval() preds = [] with torch.no_grad(): # 原图预测 preds.append(torch.softmax(model(img_tensor.unsqueeze(0).cuda()), dim=1)) # 水平翻转 preds.append(torch.softmax(model(torch.flip(img_tensor, dims=[2]).unsqueeze(0).cuda()), dim=1)) # 小角度旋转用 torchvision 的 rotate,这里简化为两次不同 crop 的预测 for _ in range(n_aug - 2): preds.append(torch.softmax(model(img_tensor.unsqueeze(0).cuda()), dim=1)) return torch.stack(preds).mean(dim=0).argmax(dim=1)

第二个技巧是分层学习率。backbone 浅层用更小的学习率(1e-5),深层和 fc 用较大的学习率(1e-4),这样浅层的通用特征不会被破坏,深层能快速适应面料任务。实现时把参数分组传给优化器,param_groups里每组设不同 lr。这个技巧在棉麻和皮质这种细粒度区分上效果明显,我实测能再涨 1 个点左右。

最后说个我自己的习惯:每次跑完实验,不管准确率多高,我都会把混淆矩阵和误判样本单独存一个文件夹,隔几天再看一遍。很多时候模型犯的错是有规律的,比如所有误判都发生在强侧光下,那就知道该补什么数据了。这份 1900 张的五分类数据集是个很好的起点,但真正决定上线的不是模型结构,而是你对这五类面料视觉特性的理解深度。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询