资讯中心

BP神经网络实现衣服图像分类:从数据预处理到手写训练代码

📅 2026/9/28 9:17:21
BP神经网络实现衣服图像分类:从数据预处理到手写训练代码
简介资源包提供一套基于BP神经网络的衣服类别识别MATLAB实现面向图像分类初学者与模式识别课程设计者。项目以衣物图片为对象覆盖图像预处理、特征提取、BP网络结构搭建、训练参数调节、模型评估与混淆矩阵分析等关键环节并延伸至电子商务、虚拟试衣间的应用场景。压缩包共433个文件约13.85MB含398张jpg衣物样本图片、19个mat数据文件和16个.m源码脚本jpg构成训练与测试样本集mat保存特征矩阵或网络状态m文件为带注释的可运行代码目录结构清晰便于逐步复现。目前已有147人学习下载。代码注释完整可直接运行训练与测试也适合调整学习率、动量项、隐藏层节点数做调优实验是理解BP反向传播机制及MATLAB图像分类工作流的实用入门资料。1. BP神经网络做衣服分类为什么这个老模型仍然值得上手把一批衣服照片丢给BP神经网络训练完在验证集上只跑出50%出头的准确率很多人第一反应是模型太老、该换卷积网络。实际踩过几次就会发现问题常常不在网络结构而在数据预处理和训练参数上。BP神经网络做衣服分类一个带足够宽隐藏层的三层网络就能扛下来图像像素向量进输入层经隐藏层加权求和、激活函数映射再由输出层吐出各类概率反向传播把误差一层层送回权重。用Python把这一套从零实现一遍比直接调框架更能看清每个环节在动什么。这篇笔记从数据准备、网络搭建、参数配置、避坑排查到结果验证逐段展开每个环节都给能直接运行的代码适合两类人急着落地衣服识别方案的开发者以及想摆脱调参黑匣子的学习者。2. 衣服图像数据准备目录结构、尺寸统一与归一化的实测做法数据准备是这类项目里最容易被低估的一环。很多“代码完整、数据齐全”的衣服分类项目跑不起来十有八九不是模型写错而是数据加载和预处理里有隐藏问题。这一章先解决数据怎么进内存、像素怎么进网络再讨论要不要做图像分割。2.1 数据集目录与标签映射加载前先约定类别顺序先约定目录结构。最常见的做法是把类别名作为子目录名训练集和验证集分开存放我一般会按下面的布局整理。dataset/ ├── train/ │ ├── T恤/ │ ├── 长裤/ │ ├── 裙子/ │ ├── 外套/ │ └── 鞋子/ └── val/ ├── T恤/ ├── 长裤/ ├── 裙子/ ├── 外套/ └── 鞋子/加载时最大的坑是标签映射不稳定。os.listdir在不同文件系统下顺序不一定相同直接用目录枚举的索引当标签训练和验证可能对不上。稳妥做法是先把类别名排序再按这个顺序生成标签。import os import numpy as np from PIL import Image def load_images_from_dir(data_dir, target_size(64, 64), use_grayTrue): images, labels [], [] # sorted保证类别顺序跨机器稳定这是标签映射的第一道保险 classes sorted(os.listdir(data_dir)) class_to_label {name: i for i, name in enumerate(classes)} for class_name in classes: class_dir os.path.join(data_dir, class_name) if not os.path.isdir(class_dir): continue for fname in sorted(os.listdir(class_dir)): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue img Image.open(os.path.join(class_dir, fname)).convert(RGB) img img.resize(target_size) if use_gray: arr np.array(img.convert(L)) / 255.0 else: arr np.array(img) / 255.0 images.append(arr.reshape(-1)) labels.append(class_to_label[class_name]) return np.array(images), np.array(labels), classes X_train, y_train, classes load_images_from_dir(dataset/train) X_val, y_val, _ load_images_from_dir(dataset/val) print(X_train.shape, y_train.shape, classes)上述代码里有两个参数值得细看。use_grayTrue会把RGB三通道图像转成单通道灰度64×64的输入特征维度从12288降到4096训练速度提升明显但对颜色敏感的任务会损失信息。reshape(-1)的作用是把二维图像展开成一维向量因为BP网络不接受二维输入这一步不做后面矩阵乘法会直接报维度错误。训练集和验证集的划分也有讲究。同一件衣服的不同角度照片或者同一个款式的不同颜色不要同时出现在训练集和验证集里。现实中很多数据集按目录存放目录里恰好全是同一个款式的多张图直接按目录坐标切开会让验证集“作弊”成绩虚高。我一般会按衣服个体分组后再拆而不是简单按文件比例切。2.2 尺寸统一与像素归一化两个容易顺手做错的预处理衣服照片的来源各异有商品白底图、有街拍图、有模特图尺寸和比例都不一样。BP网络要求输入维度固定所以第一步是统一尺寸。常见做法是直接缩放到64×64或96×96作物图比更复杂时可以先等比缩放再中心裁剪避免衣服主体被拉变形。实际项目里如果数据集本身是白底商品图直接resize问题不大有街拍大图时建议先做一次主体裁剪。灰度还是RGB要看任务本身下面这个对比表可以帮你做选择。输入方案特征维度训练一个epoch耗时颜色信息适用场景灰度64×644096快无衣服轮廓差异大灰度96×969216中无需要保留更多纹理细节RGB 48×486912中有颜色在类别间有明显区分RGB 64×6412288慢有数据量小且类别靠颜色区分像素归一化很容易被忽略。很多新手直接把像素值0-255送进网络结果损失函数一路震荡。原因很简单BP网络的权重初始化、学习率都是按小数值输入设计的输入范围太大时隐藏层的加权和会非常大激活函数容易进入饱和区梯度接近消失训练自然卡住。# 归一化到[0,1]是BP网络能正常收敛的前提 arr np.array(img) / 255.0 # 如果样本量够大也可以进一步做标准化 # arr (arr - mean) / std # 注意mean和std要由训练集统计验证集和预测时复用同一组值归一化之后建议顺手打印几个样本的像素统计值确认范围落在0到1之间。这个检查看起来多余实际能省下好几轮排查时间。2.3 图像分割要不要做背景干扰严重时先切出主体这一节聊的是图像分割在BP衣服分类前期的作用。检索“BP神经网络 图像分割”时能看到不少项目把分割当作预处理手段常见思路是把图像分割、主体提取和分类串联起来。BP网络的输入是一个扁平向量背景像素和衣服像素在向量里是平等的。如果训练集里背景模式恰好和类别强相关比如裙子的照片背景偏浅色、外套的背景偏深色网络就会偷懒去学“背景亮度”而不是“衣服形状”。这种情况下换更复杂的分类网络也无济于事因为欺骗性特征已经浸入数据了。对于白底图或单一背景的商品图最简单的做法是用边缘检测把主体找出来再裁剪不需要上语义分割模型。下面这个函数就是一个够用的实现。import cv2 def crop_main_object(img_bgr, pad10): gray cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(gray, 50, 150) # 取最大外轮廓的包围盒作为主体区域 contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return img_bgr x, y, w, h cv2.boundingRect(max(contours, keycv2.contourArea)) x0, y0 max(x - pad, 0), max(y - pad, 0) x1 min(x w pad, img_bgr.shape[1]) y1 min(y h pad, img_bgr.shape[0]) return img_bgr[y0:y1, x0:x1]这个函数只做了三件事转灰度、Canny找边缘、取最大轮廓的包围盒。处理白底图足够复杂背景不一定稳。要注意的一点是训练和验证必须走同一条预处理管线。如果训练时裁了主体验证时没裁验证结果就没有参考价值。复杂背景时用人体分割或前景分割模型更可靠但对于商品图数据集上面这段代码已经能解决大多数“背景偷走特征”的问题。3. 手写BP网络实现前向传播、反向传播与权重更新的核心代码这一章进入模型主体。标题里的“BP神经网络Python代码”是检索量很高的需求点我就按手写实现来拆。用numpy手写BP网络比直接套sklearn的MLPClassifier更适合理解原理而且后续调参时每一步都是可控的。我先讲结构设计再给前向和反向的完整代码。3.1 网络结构与初始化把输入层、隐藏层、输出层的维度定下来网上搜“BP神经网络结构图”看到的基本都是三层结构输入层、隐藏层、输出层。落实到衣服分类上输入层维度等于展平后的像素数灰度64×64就是4096输出层维度等于类别数5类就是5。关键在隐藏层怎么定这个在第4章展开先按经验取128。权重初始化直接决定训练能不能起步。全零初始化会让所有神经元输出相同的梯度等于白训随机初始化范围太大会让tanh进入饱和区。我一般用Xavier初始化也就是按输入维度缩放的随机值。import numpy as np class BPNet: def __init__(self, n_features, n_hidden, n_classes, lr0.01, momentum0.9): # Xavier初始化让每层信号的方差保持在同一量级避免梯度消失或爆炸 self.W1 np.random.randn(n_features, n_hidden) * np.sqrt(2.0 / n_features) self.b1 np.zeros((1, n_hidden)) self.W2 np.random.randn(n_hidden, n_classes) * np.sqrt(2.0 / n_hidden) self.b2 np.zeros((1, n_classes)) self.lr lr self.momentum momentum self.vW1 np.zeros_like(self.W1) self.vb1 np.zeros_like(self.b1) self.vW2 np.zeros_like(self.W2) self.vb2 np.zeros_like(self.b2)手动解释一下W1的维度是 输入特征数×隐藏层神经元数负责把输入向量变换到隐藏空间W2的维度是 隐藏层神经元数×类别数负责把隐藏特征映射成类别得分。sqrt(2.0 / n_features)是Xavier初始化的常见写法比直接用np.random.randn乘一个固定小数更稳。偏置初始化为0没问题不必加随机量。3.2 前向传播与损失函数softmax输出的概率从哪来前向传播的逻辑是输入X先和W1做矩阵乘法加上偏置后进tanh激活函数得到隐藏层输出a1a1再和W2相乘过softmax转成概率分布。隐藏层用tanh是因为它输出范围在-1到1之间梯度比sigmoid更稳输出层用softmax是因为多分类任务需要所有类别概率之和为1。def forward(self, X): self.z1 X.dot(self.W1) self.b1 self.a1 np.tanh(self.z1) self.z2 self.a1.dot(self.W2) self.b2 # softmax减去最大值是为了数值稳定防止exp溢出 exp_scores np.exp(self.z2 - np.max(self.z2, axis1, keepdimsTrue)) self.a2 exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) return self.a2 def compute_loss_acc(self, X, y): m X.shape[0] probs self.forward(X) # 交叉熵损失加1e-9防止log(0) loss -np.mean(np.log(probs[np.arange(m), y] 1e-9)) acc np.mean(np.argmax(probs, axis1) y) return loss, acc这里probs[np.arange(m), y]取的是每个样本真实类别对应的预测概率。交叉熵损失在预测概率接近1时趋近0在预测错误且概率很低时很大这正是分类任务想要的惩罚特性。softmax里的- np.max是纯数值稳定操作不影响最终概率结果但能避免指数计算溢出为无穷大。3.3 反向传播与训练循环最容易抄错的一段代码反向传播是整个BP网络的核心。对softmax加交叉熵的组合梯度可以简化成“预测概率减one-hot标签”这个漂亮的结果tanh的导数等于1 - a1**2。把误差逐层传回去按链式法则算出W1、W2的梯度再用动量SGD更新。def backward(self, X, y): m X.shape[0] # 输出层误差softmax交叉熵的梯度化简为 probs - one_hot g2 self.a2.copy() g2[np.arange(m), y] - 1 dW2 self.a1.T.dot(g2) / m db2 np.sum(g2, axis0, keepdimsTrue) / m # 隐藏层误差tanh导数 1 - tanh^2 g1 g2.dot(self.W2.T) * (1 - self.a1 ** 2) dW1 X.T.dot(g1) / m db1 np.sum(g1, axis0, keepdimsTrue) / m return dW1, db1, dW2, db2 def update(self, dW1, db1, dW2, db2): # 动量更新把上一次的更新方向保留一部分能抑制震荡 self.vW1 self.momentum * self.vW1 - self.lr * dW1 self.vb1 self.momentum * self.vb1 - self.lr * db1 self.vW2 self.momentum * self.vW2 - self.lr * dW2 self.vb2 self.momentum * self.vb2 - self.lr * db2 self.W1 self.vW1 self.b1 self.vb1 self.W2 self.vW2 self.b2 self.vb2 def train_one_epoch(self, X, y, batch_size32): # 每个epoch内随机打乱样本再按mini-batch梯度更新 perm np.random.permutation(X.shape[0]) for i in range(0, X.shape[0], batch_size): idx perm[i:i batch_size] dW1, db1, dW2, db2 self.backward(X[idx], y[idx]) self.update(dW1, db1, dW2, db2)这段代码里的g2[np.arange(m), y] - 1是整段反向传播中最容易被抄错的位置。它把预测概率向量中真实类别对应的位置减1其余位置保持不变这正是同一个样本的梯度方向。如果你把这一句写成了对全体概率都减1输出层的梯度就全错了训练再久也不收敛。训练循环也很简单按epoch组织每个epoch内打乱样本顺序再按批更新。model BPNet(n_featuresX_train.shape[1], n_hidden128, n_classeslen(classes), lr0.01) for epoch in range(100): model.train_one_epoch(X_train, y_train, batch_size32) if epoch % 20 0: loss, acc model.compute_loss_acc(X_train, y_train) print(fepoch {epoch}, loss{loss:.4f}, acc{acc:.4f})批量大小在numpy实现里影响的是梯度估计的平稳性。32是一个兼顾速度与稳定性的默认值数据量只有几百张时可以用全批量训练梯度更稳。这里的batch_size32指的是每个批次32张图不是类别数。4. 训练参数调优学习率、隐藏层神经元数与早停怎么配合很多衣服分类项目翻车不是模型结构写错而是参数没配对。这一章把最有影响的几个训练参数讲透。注意这个环节看上去有点“玄学”其实每条规律背后都有梯度传播的逻辑支撑。4.1 学习率与动量项损失震荡多半是这两个值没配对学习率是BP网络里最敏感的参数。设大了权重一次跨得太远损失来回震荡设小了训练几十个epoch看不出变化。衣服分类这种输入特征上千、样本量几千的任务我一般从0.01起步跑50个epoch看损失曲线再调整。学习率典型表现适用情况0.001收敛慢但过程稳定数据集小、类别多、损失容易爆0.01收敛速度和稳定性均衡默认起点0.1损失容易前几步就飞掉很少直接使用动量项的作用是让梯度更新方向保持惯性抑制震荡。代码里已经实现了动量缓冲变量vW1默认0.9是一个通用的稳妥值。如果你发现损失曲线震荡剧烈但整体下降可以先不动学习率把动量从0.9提到0.95试试如果震荡完全没有下降趋势那多半是学习率偏高降到0.005或0.001。4.2 隐藏层神经元数量经验公式之外还得看验证集脸色隐藏层神经元数量既关系到模型容量也和训练时间强相关。网上流传的经验公式不少常见的有取输入维度平方根、取输入和输出维度乘积的平方根等。对64×64灰度输入、5类输出来说这些公式给出的区间大概在几十到一百多之间实战里128是个很好的起点。判断该加宽还是收窄唯一的客观标准是验证集表现。训练集准确率能到95%以上验证集只有70%说明模型容量超出数据承载能力属于过拟合这时候增加神经元只会更糟。反过来训练集和验证集准确率同步偏低比如都在60%左右说明模型学不动可以尝试把神经元从128加到256同时把学习率稍微调小。在小数据集上隐藏层超过256个神经元几乎一定会带来过拟合。因为衣服图像的特征维度虽然大但有效信息往往集中在轮廓和纹理上样本量不够时过宽的隐藏层会把噪声也一并记住。我一般会在128和256之间做一组对比实验而不是试探各种奇怪的数字。4.3 训练轮数、批大小与早停训练到什么程度该收手BP网络在家用CPU上训练几百个epoch通常几分钟就完事可实际操作中训练越久验证集不一定越好过拟合往往发生在训练后期。早停机制就是这个时候的后悔药它的逻辑很简单如果验证集准确率连续若干轮不增长就停止训练并且恢复历史最优权重。best_acc 0.0 patience 0 best_weights None for epoch in range(200): model.train_one_epoch(X_train, y_train, batch_size32) val_loss, val_acc model.compute_loss_acc(X_val, y_val) if val_acc best_acc: best_acc val_acc patience 0 best_weights (model.W1.copy(), model.b1.copy(), model.W2.copy(), model.b2.copy()) else: patience 1 if patience 10: print(fepoch {epoch}: early stop, best_acc{best_acc:.4f}) break if best_weights is not None: model.W1, model.b1, model.W2, model.b2 best_weights这段代码里patience10表示连续10轮验证集准确率没有刷新记录就停。序列中的每次epoch都进行训练但BEST权重保存在best_weights里结束之后再把最优权重放回模型避免把最后几个过拟合的epoch当成可用模型。训练轮数从哪开始调我会先跑100个epoch观察如果损失还在明显下降就继续往后跑如果早停触发了那就说明当前模型容量、数据量和超参数已经到达上限不必强行延长训练。5. BP衣服分类避坑清单五个高发翻车点与排查办法这一章是从实战经验里提练出来的踩坑点每条都按“现象→原因→解决”的顺序写。如果你按前面章节跑通了训练但结果不理想建议对照这里的清单逐条排查。5.1 现象一损失震荡不收敛训练几千轮损失还在大幅跳变传入网络的仍是0-255像素值或者学习率设得过高初始化范围过大是最常见的原因。检查办法很简单第一轮训练前打印X_train.min()和X_train.max()确认值在0到1之间。如果归一化没问题就把学习率降到0.005再试。级联的gradient clip可快速缓解异常样本造成的梯度拉飞。训练中权重更新前顺便裁剪梯度# 梯度裁剪防止个别异常样本把权重一次拉飞 for grad in [dW1, dW2]: norm np.linalg.norm(grad) if norm 1.0: grad / norm注意梯度裁剪只是兜底手段不能替代学习率调整。如果裁剪后损失依然震荡问题大概率出在数据归一化或标签错乱。5.2 现象二训练集准确率很高验证集长期在低位徘徊这是典型的过拟合也可能源于训练集和验证集的分布不一致。比如衣服图像按款式存放同一个款式的多种角度图被同时划分进两个集合验证就会虚高反过来如果验证集里出现了训练集中从没见过的拍摄背景成绩会普遍低于预期。解决分两步先检查数据切分确保同一件或同一款式的图像不跨集合再启用早停把训练截在验证集最优点的位置。如果过拟合仍然严重把隐藏层从256收缩到128或者对W1加L2正则化约束都能压低模型容量。5.3 现象三同款衣服换个背景颜色就分错模型学到了“背景色→类别”的捷径而不是衣服本身的特征。这常见于训练图背景单一比如白底图和浅灰底图混着验证时换到深色背景就乱了。初次遇到这种问题以为模型坏了实际是数据分布太窄。解决优先级从低到高把输入改为灰度图切断颜色通道的干扰通道对训练集做背景多样性扩充或者用第二章的crop_main_object先把主体切出来。前提是类别本身不靠颜色区分。如果任务定义的类别是“红色连衣裙”这一类的颜色恰恰是判据那就不能直接灰度化而应做背景一致性处理。5.4 现象四一张图里多个主体时分类结果被带偏街拍图或模特图里常有背景人物或衣架上的其他衣服BP网络把整张图的像素都当输入前景衣服占比小特征会被稀释。现象就是分类结果总往视觉面积大的那个物体上跑。最可靠的做法是训练和预测统一走主体裁剪流程。边缘检测在复杂场景有时不生效可以加一个面积阈值把过小的轮廓过滤掉只保留最大主体。预测阶段尤其要注意不能训练时裁了主体、预测时不裁同一张图在不同管线下效果会完全不同。5.5 现象五同一套模型换台机器预测结果对不上权重文件明明同一个预测结果却和训练时不一致多半是预处理不一致。比如训练时用PIL的convert(L)转灰度预测时OpenCV的cvtColor转灰度两者的灰度计算公式不同像素值就有差异模型输出自然变了。还有一种情况是保存模型时没保存类别顺序加载后classes是乱序的预测结果张冠李戴。解决办法是把“读取→缩放→转灰度→归一化→展平”封装成一个函数训练和预测代码都调用同一个函数不要在别处重新写一遍。另外保存模型时把classes数组一起写入文件加载后立即解出来对应。6. 验证与交付用混淆矩阵看分类短板再保存模型跑通预测训练结束只代表模型收敛了不代表它可用。验证阶段别只盯着总准确率用混淆矩阵看逐类的错分模式这一步能暴露总准确率掩盖的问题。from sklearn.metrics import confusion_matrix, classification_report probs model.forward(X_val) y_pred np.argmax(probs, axis1) cm confusion_matrix(y_val, y_pred) print(cm) print(classification_report(y_val, y_pred, target_namesclasses, zero_division0))混淆矩阵的行是真实类别列是预测类别。方阵对角线上数值越大越好对角线以外的块就是错误模式。比如“裤子”和“裙子”互相看错多是因为轮廓相似有点“外套”被错分为“鞋子”则要先怀疑标签文件是否配错。分类报告里的precision、recall和support也要扫一眼某类样本量过少时即使该类完全分错总准确率也看不太出来。模型验证通过后把权重保存下来。保存格式用npz就够不需要引入额外依赖。注意同时保存类别顺序否则预测时无法把输出位置映射回衣服名称。# 保存模型权重与类别顺序预测时保证标签映射一致 np.savez(bp_clothes_model.npz, W1model.W1, b1model.b1, W2model.W2, b2model.b2, classesnp.array(classes))加载和单张预测的代码和训练共用一套预处理函数即可最关键的是归一化和灰度转换逻辑必须完全一致。def preprocess_single_image(img_path, target_size(64, 64), use_grayTrue): img Image.open(img_path).convert(RGB).resize(target_size) if use_gray: return np.array(img.convert(L), dtypenp.float32) / 255.0 return np.array(img, dtypenp.float32) / 255.0 loaded np.load(bp_clothes_model.npz) model.W1, model.b1, model.W2, model.b2 loaded[W1], loaded[b1], loaded[W2], loaded[b2] classes_loaded list(loaded[classes]) x preprocess_single_image(test_skirt.jpg).reshape(1, -1) prob model.forward(x)[0] top_idx np.argsort(prob)[::-1][:3] # 输出概率最高的前3个类别 for idx in top_idx: print(classes_loaded[idx], prob[idx])这里单张图先走完全一致预处理然后reshape(1, -1)补上批次维度再送进model.forward。把前3个候选类输出而不是只看最大值能在现场排查时提供更多信息。我自己的习惯是每完成一轮“数据整理→训练→验证”的全流程都会把数据加载和预处理的脚本固定下来作为以后每个项目的模板。这个流程里的多数失败其实都出在“处理不一致”这类小事上而不是模型复杂度不够。用这套方法完成一次衣服分类之后再回头换成PyTorch或sklearn会发现所有参数经验都能无缝平移。希望这些踩坑记录帮你在自己的数据集上少走几趟弯路。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案