
简介基于SVM的焊接缺陷图片识别分类Python项目面向计算机视觉与机器学习入门学习也可作为毕业设计、课程设计、大作业或初期项目演示的原型。压缩包内共包含144个文件以140张焊接缺陷PNG样本图、3个Python源码脚本和1个zip子包为主整体约1.7MB。数据集覆盖未焊透、未熔合等典型缺陷类别可支撑“图像读取—特征提取—SVM训练—分类评估”完整流程由于需自行训练模型读者能借此理解支持向量机在小型图像分类任务中的实际调参与效果。目前已有518人学习下载。项目代码经测试运行成功结构简洁、依赖较少适合在此基础上继续扩展更多缺陷类别或优化特征方案也可作为同类图像分类任务的模板具备较强的学习借鉴与二次开发价值。适用人群覆盖计算机相关专业在校学生、教师及企业研发人员可用作机器学习课程作业或小样本图像分类演练。1. 焊接缺陷图片分类为什么选 SVM 而不是一上来就上深度学习拿到这个基于SVM实现焊接缺陷图片识别分类Python源码时很多人第一反应是都 2025 年了图像分类不都应该用 CNN 或者 YOLO 吗但等你真的处理过一批产线上的焊接缺陷图片就会发现多数实际场景下你能拿到的有效缺陷样本可能只有几百张有些类别比如未熔合和裂纹甚至只有几十张。这种规模的数据扔给深度学习模型基本就是看它表演过拟合。SVM 在这种小样本、高维特征、类别界限相对清晰的分类任务上反而更容易快速得到一个稳定可用的结果而且训练只需要 CPU几分钟就能跑完。这篇笔记就围绕这个标题里的完整链路来讲图片怎么预处理、特征怎么提取、SVM 参数怎么调、以及哪些地方最容易让你翻车。适合手里已经有一批焊接缺陷图片、想先跑通一个可用的分类流程、又不想被深度学习环境折腾半天的工程师。2. SVM 识别焊接缺陷的原理与选型线性不可分、核函数和图像特征从哪来2.1 从最大间隔理解 SVM 为什么适合小样本分类SVM 的核心思想不是把所有的点都分对而是找一个能把两类样本分开、并且离两类样本都尽可能远的超平面。这个离得远就是间隔最大化间隔越大模型对新样本的泛化能力通常越好。和神经网络不同SVM 的决策函数只由支持向量决定也就是说真正影响模型边界的只是离分界线最近的那一小部分样本而不是全部数据。这个特性对小样本分类非常重要。焊接缺陷图片里气孔和夹渣在视觉特征上往往有明显区别但样本数量不多且不同批次图片的光照条件差异很大。SVM 不需要大量数据去喂出一个复杂的特征提取器它只需要你在特征工程阶段把每张图变成一组有区分度的数值向量剩下的分类边界由支持向量撑起来。当两类样本在原始特征空间里线性不可分时SVM 通过核函数把数据映射到高维空间在高维空间里找一个线性超平面。RBF 核是实际项目里最常用的选择因为它只有一个 gamma 参数需要调节而且能处理绝大多数非线性边界。你不需要理解太深的数学推导但必须知道 C 和 gamma 这两个参数分别控制什么否则后面调参就是纯靠运气。2.2 焊接缺陷图片需要什么样的特征从像素到向量的关键一步SVM 不能直接吃一张二维图片它吃的是一维特征向量。把图片变成向量的过程决定了整个分类效果的上限。深度学习模型是自己在训练过程中学特征而 SVM 需要你手工指定特征。这也是很多人用 SVM 做图像分类效果差的最主要原因——特征选得不对换什么核函数都没用。焊接缺陷图片常见的有气孔、裂纹、夹渣、未熔合、咬边这几类。气孔在图像上表现为圆形暗斑夹渣是不规则亮块裂纹是细长线条未熔合通常出现在焊缝边缘。针对这些视觉差异常用的特征有三类一是灰度直方图特征。把图片转成灰度图统计 0 到 255 每个灰度级别的像素数量得到一个 256 维的向量。这个特征能区分整体明暗差异大的类别比如气孔偏暗和夹渣偏亮但对形状不敏感。二是 HOG 特征即方向梯度直方图。它统计图像局部区域内梯度方向的分布对边缘和纹理敏感适合捕捉裂纹这种细长线条、以及气孔和夹渣的轮廓差异。HOG 特征的维度通常在几百到几千之间需要配合降维或者直接交给 SVM 处理。三是形状特征比如缺陷区域的面积、周长、圆形度、最小外接矩形的长宽比。这些特征需要你先对图片做阈值分割或边缘检测把缺陷区域从背景里分离出来然后计算区域属性。实际项目中很少有人只用一种特征。我一般会先把图片做直方图均衡化然后拼接灰度直方图和 HOG 特征得到一个组合向量。如果你的图片背景很复杂、焊缝区域占了很小的比例就要先裁剪出 ROI 区域再做特征提取否则大量背景像素会稀释掉缺陷本身的特征。2.3 SVM 与 CNN 的选型对比不是越先进的越好很多人在做图像分类时默认选择卷积神经网络但 CNN 有三个硬性前提足够多的标注样本、GPU 算力、以及相对长的训练调参周期。焊接缺陷检测这个场景恰恰三个条件经常都不满足。一个现实的对比是这样的如果你有 500 张带标注的焊接缺陷图片用 ResNet 这类预训练模型做迁移学习效果不一定比 SVM 好因为预训练模型是在 ImageNet 的自然图像上学的特征焊缝的灰度纹理和自然图像差异很大迁移过来的特征不一定有用。而 SVM 配合手工特征在几百张样本上基本能收敛到 85% 到 95% 的准确率训练时间以秒或分钟计。如果你的数据量能到几千张甚至上万张且有 GPU 环境那么用 YOLO 系列做目标检测、或者用 ResNet 做分类会更合适。但在标题给定的这个场景——Python 源码、含数据集、需要自己训练模型——SVM 路线的优势在于它不依赖 GPU也不要求你有深度学习调参经验拿到数据集后先做特征提取再用 sklearn 里的 SVC 跑一遍就能得到基线结果。还有一个实际考虑是模型的可解释性。SVM 训练完以后你可以直接查看支持向量的数量、输出决策函数的间隔分布分析哪些特征对分类贡献最大。深度学习模型在这方面几乎是个黑匣子出了问题很难定位是数据的问题还是模型的问题。2.4 用一张参数表快速理解 SVM 的三个核心选择参数/选项常见取值作用调节倾向kernelrbf / linear / poly决定数据映射到高维空间的方式默认选 rbf线性可分时可换 linearC0.1 到 1000误分类的惩罚程度C 越大越容易过拟合越小越偏向平滑边界gamma0.0001 到 10RBF 核的影响半径gamma 越大决策边界越复杂越小越平滑这三个参数是 SVM 调参的主战场。kernel 决定用什么方式切分C 决定允许分错多少gamma 决定每个样本影响范围有多大。后面第 4 章会详细讲怎么用网格搜索把这些参数调出来这里先建立概念。3. 图片预处理与特征提取把焊接缺陷图片转成 SVM 能吃的向量3.1 处理前先看数据统一尺寸、灰度化和归一化拿到图片数据集的第一步不是写代码而是先看数据。把每个类别的图片都打开看一遍统计图片的尺寸分布、亮度分布、是否存在大量与缺陷无关的背景区域。焊接缺陷图片通常来自 X 射线探伤设备或工业相机图片尺寸可能不统一而且很多是 16 位深度的灰度图直接读进来会被当作普通 8 位图处理导致灰度信息丢失。常见做法是先把所有图片统一缩放到一个固定尺寸比如 224x224 或者 128x128。尺寸不宜太大因为 HOG 特征的维度会随图片尺寸成倍增长SVM 在高维特征下训练速度会明显下降也不宜太小否则裂纹这类细长缺陷会被压缩到看不见。我一般取 160x160这是一个在特征质量和计算量之间比较平衡的尺寸。import cv2 import os def load_and_preprocess(img_path, target_size(160, 160)): # 按原始格式读取保留16位灰度深度 img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if img is None: raise ValueError(f无法读取图片: {img_path}) # 如果读进来是彩色图先转灰度 if len(img.shape) 3: img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 统一缩放尺寸interpolation选INTER_AREA更适合缩小图片 img cv2.resize(img, target_size, interpolationcv2.INTER_AREA) # 直方图均衡化增强对比度 img cv2.equalizeHist(img) # 归一化到0~1范围方便后续特征计算 img img.astype(float32) / 255.0 return img这段代码做了四件事按原始格式读图、转灰度、缩放、直方图均衡化。IMREAD_UNCHANGED 这个参数很关键如果原图是 16 位深度而你不加这个参数OpenCV 会默认按 8 位读取高灰度区的细节直接丢失。直方图均衡化是焊接图片处理里性价比最高的一步因为 X 射线探伤图经常整体偏暗且对比度不足缺陷区域和背景的灰度差异被压缩得很小均衡化能显著拉开差距。归一化到 0 到 1 范围值得提醒如果不做归一化灰度直方图的数值范围是 0 到 255而 HOG 特征的值通常很小两者拼接后数量级差几百倍SVM 的距离计算会被大数值特征主导。虽然后面 SVM 内部有缩放处理但提前归一化只有好处没有坏处。3.2 用灰度直方图和 HOG 特征做图像特征工程接下来把预处理好的图片转成特征向量。灰度直方图的实现很简单一行 numpy 就能搞定。HOG 特征用 skimage 库里的 hog 函数提取需要调几个参数orientations 是梯度方向的分桶数pixels_per_cell 是每个细胞单元的像素大小cells_per_block 是每个块包含的细胞单元数。import numpy as np from skimage.feature import hog def extract_features(img): # 灰度直方图统计16个bin的灰度分布 hist np.histogram(img, bins16, range(0, 1))[0] # 归一化直方图避免图片明暗影响数值大小 hist hist / hist.sum() # HOG特征8个方向8x8像素一个cell每个block包含2x2个cell hog_feat hog( img, orientations8, pixels_per_cell(8, 8), cells_per_block(2, 2), block_normL2-Hys, feature_vectorTrue, ) # 拼接两部分特征 features np.concatenate([hist, hog_feat]) return features这段代码里灰度直方图取了 16 个 bin 而不是 256 个原因是一张 160x160 的图片里缺陷区域的像素占比本来就小分 256 个 bin 会把很多相邻灰度的统计打散反而不利于分类。16 到 32 个 bin 在区分度和稳定性之间比较合适。HOG 参数的设置直接影响特征维度和分类效果orientations8 意味着把梯度方向分成 8 个区间pixels_per_cell 越大特征越粗糙cells_per_block 越大对光照变化的鲁棒性越好。这里有一个参数联动关系要注意图片尺寸变了HOG 特征维度也会变。160x160 的图pixels_per_cell(8,8) 时横向和纵向各有 20 个 cell最终 HOG 特征维度是 20x20x2x2x8约 12800 维。加上 16 维直方图特征每条样本是一个 12816 维的向量。这个维度不算低但 SVM 对高维稀疏特征并不敏感关键是样本量要撑得住——你的样本数不能比特征维度小太多否则必然过拟合。如果样本只有两三百张可以考虑把图片缩到 96x96或者把 pixels_per_cell 调到 (16,16)把特征维度降到 3000 以内。3.3 批量生成特征向量并保存为 numpy 文件单张图片的特征提取没有意义要做的是把整个数据集的图片批量转成特征矩阵然后保存下来。训练集和测试集在特征提取阶段必须使用完全相同的参数否则维度对不上而且分布也会不一致。import numpy as np import os import pickle from sklearn.preprocessing import LabelEncoder def build_feature_dataset(img_dir, target_size(160, 160)): X, y [], [] # 假设目录结构是数据集根目录/类别名/图片文件 categories sorted([d for d in os.listdir(img_dir) if os.path.isdir(os.path.join(img_dir, d))]) for label_idx, category in enumerate(categories): category_path os.path.join(img_dir, category) for fname in os.listdir(category_path): if not fname.lower().endswith((.jpg, .png, .bmp, .tif)): continue img_path os.path.join(category_path, fname) img load_and_preprocess(img_path, target_size) feat extract_features(img) X.append(feat) y.append(label_idx) X np.array(X) y np.array(y) return X, y, categories # 执行特征提取 X, y, categories build_feature_dataset(./welding_defect_dataset) print(特征矩阵形状:, X.shape, 标签数量:, len(y)) print(类别列表:, categories) # 保存为numpy压缩格式 np.savez_compressed(welding_features.npz, XX, yy, categoriescategories)这段代码把类别名按字母序映射成整数标签然后遍历每个类别目录下的所有图片做预处理和特征提取最终得到一个形状为 (样本数, 12816) 的特征矩阵和对应的标签数组。保存成 npz 文件的好处是后续训练时不用重新提取特征能省掉大量重复计算。代码里的 categories 排序是一个容易忽略的细节。如果直接用 os.listdir 的顺序不同机器上遍历顺序可能不一致导致同一个类别在不同运行中被赋予不同标签。用 sorted 排序可以保证标签的确定性。实际项目里更稳妥的做法是用 LabelEncoder 显式建立类别名和整数标签的映射关系并把映射表也保存下来后面预测新图片时要用同一个映射。3.4 特征维度过高时的处理PCA 降维要不要用特征矩阵的维度到了 12000 以上而样本量只有几百时很多人会担心维度灾难随手加一个 PCA 降到 50 维。我的经验是先不要急着降维直接用原始特征跑一遍 SVM 看结果。sklearn 的 SVC 内部实现会做特征缩放高维度本身不是问题真正的问题是样本量。如果训练后发现了明显的过拟合比如训练集准确率 99% 而测试集只有 70%这时再考虑降维。PCA 降维适合处理两类情况一是特征之间存在大量冗余HOG 特征里相邻维度的相关性本来就高二是你想把训练速度提上来特别是后面用网格搜索调参时12000 维特征在几百个参数组合下训练会非常慢。from sklearn.decomposition import PCA # 先标准化再PCASVM对特征尺度敏感 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_scaled scaler.fit_transform(X) # 保留95%方差的主成分 pca PCA(n_components0.95) X_pca pca.fit_transform(X_scaled) print(PCA降维后维度:, X_pca.shape[1])PCA 保留 95% 方差一般能把 12000 维降到 100 到 300 维计算量是原来的几十分之一。但要注意PCA 是在整个数据集上拟合的如果后面要做交叉验证PCA 必须在每一折的训练集上重新拟合不能在全体数据上先降维再划分训练测试集否则会引入数据泄露测试集的信息在训练阶段就已经被看到了。这一点等第 5 章避坑部分会再展开。4. 用 Python 训练 SVM 焊接缺陷分类器训练脚本、参数调节与模型保存4.1 数据划分训练集和测试集怎么分才靠谱特征提取完成后就到了训练环节。训练的第一步不是实例化 SVM而是划分数据集。焊接缺陷场景里同一张图片的轻微缩放、旋转版本很可能同时出现在原始数据集中如果直接随机划分可能会导致同源图片同时出现在训练集和测试集里测试分数虚高。import numpy as np from sklearn.model_selection import train_test_split # 加载之前保存的特征 data np.load(welding_features.npz, allow_pickleTrue) X, y data[X], data[y] categories data[categories] # 按类别分层划分保证每个类别在训练和测试里都有 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(训练集样本数:, X_train.shape[0]) print(测试集样本数:, X_test.shape[0]) for cat in range(len(categories)): print(f类别 {categories[cat]}: 训练 {np.sum(y_traincat)} 张, 测试 {np.sum(y_testcat)} 张)stratifyy 这个参数是分层划分的关键它保证训练集和测试集里每个类别的比例和原始数据集一致。焊接缺陷数据的类别分布通常很不均衡比如气孔占了 60%咬边只占 5%如果不分层小类别可能全部落进训练集或全部落进测试集导致模型对小类别完全失效。random_state42 固定随机种子也是必须做的否则每次运行划分结果都不一样你就没法复现实验、也没法判断参数变化到底有没有用。数据划分是整个训练流程的基石很多人后面发现模型分数忽高忽低根因往往是每次划分的数据不同而不是模型的锅。4.2 先用默认参数跑通基线SVC 的基本用法拿到划分好的数据先不要调参用 sklearn 的默认参数跑一遍得到基线准确率。这一步的意义是确认整个流程是通的——特征提取没问题、数据划分没问题、SVM 能正常收敛。from sklearn.svm import SVC from sklearn.metrics import accuracy_score, classification_report # 先用默认的RBF核跑基线 model SVC(kernelrbf, C1.0, gammascale, class_weightbalanced) model.fit(X_train, y_train) # 训练集和测试集分别评估 y_train_pred model.predict(X_train) y_test_pred model.predict(X_test) train_acc accuracy_score(y_train, y_train_pred) test_acc accuracy_score(y_test, y_test_pred) print(f训练集准确率: {train_acc:.4f}) print(f测试集准确率: {test_acc:.4f}) # 打印每个类别的精确率和召回率 print(classification_report(y_test, y_test_pred, target_namescategories))默认参数里值得注意的有一个gammascale它的含义是 gamma 的取值会根据特征数量自动计算等于 1 除以特征维数乘以特征方差。这个设置比手动指定一个固定值更安全因为特征维度不同时合理的 gamma 范围差异很大手动指定 0.1 在低维特征上可能有效在高维特征上就可能让决策边界过于复杂。class_weightbalanced 是我强烈建议加上的参数。焊接缺陷数据集中裂纹和未熔合往往只有几十张样本其余类别各有几百张此时如果不做类别加权SVM 会为了整体准确率牺牲小类别的分类效果。设置为 balanced 后每个类别的权重和样本数量成反比小类别在损失函数里占的权重更大。基线结果出来后看两个指标训练集准确率和测试集准确率的差距。如果两者都低说明特征本身区分度不够需要回第 3 章调特征如果训练集高而测试集低说明过拟合需要调整 C 和 gamma。4.3 调参实战用 GridSearchCV 找到最合适的 C 和 gamma基线跑通后进入调参环节。SVM 最关键的参数就三个C、gamma、kernel。网格搜索是最简单可靠的调参方式它会把所有参数组合都训练一遍用交叉验证分数选出最优组合。但要注意网格搜索的计算量和特征维度、样本量、参数组合数成正比12000 维特征加上几十个参数组合可能要跑十几分钟到半小时所以先用第 3 章提到的 PCA 把维度降下来或者缩小搜索范围。from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC # 定义参数搜索空间 param_grid { C: [0.1, 1, 10, 100], gamma: [0.001, 0.01, 0.1, 1, scale], kernel: [rbf], } # 交叉验证网格搜索 grid GridSearchCV( SVC(class_weightbalanced), param_grid, cv5, scoringf1_macro, n_jobs-1, verbose1, ) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优交叉验证得分:, grid.best_score_) print(测试集准确率:, accuracy_score(y_test, grid.best_estimator_.predict(X_test)))网格搜索里有几个参数要说明。cv5 表示五折交叉验证每一折都是把训练集再分成 5 份轮流用 4 份训练、1 份验证。这样评估出来的分数比单次划分更稳定不容易被某一次划分的偶然性影响。scoring 用 f1_macro 而不是 accuracy是因为数据类别不均衡准确率会掩盖少数类完全分错的问题宏平均 F1 对每个类别的表现一视同仁。n_jobs-1 让所有 CPU 核心并行计算能显著缩短时间。搜索空间中 C 有 5 个取值、gamma 有 5 个取值一共 25 个组合每个组合跑 5 折交叉验证也就是总共训练 125 次 SVM。如果每次训练要 5 秒总耗时约 10 分钟。体感上如果超过 30 分钟就说明特征维度太高了应该回去降维或调整 HOG 参数。一个实际的经验是先用粗网格C 取 [0.1, 1, 10, 100]gamma 取 [0.01, 0.1, 1]快速定位一个大致范围然后在最优值附近用更细的网格再跑一次。比如粗网格结果发现 C10 最好第二步就试 C 在 [5, 8, 10, 12, 15] 之间。这样能避免一开始就用非常细的网格把时间全耗在明显不是最优的区域上。4.4 常见问题SVM 训练不收敛或报错用 sklearn 的 SVC 训练时不太会遇到不收敛的问题因为它内部用的是 SMO 算法而不是梯度下降类库已经处理了收敛性问题。但你可能会遇到下面两类报错一是特征包含 NaN 或 Inf。如果某张图片损坏、读取失败后没有被跳过而是直接参与了特征计算特征向量里就可能有 NaN。SVM 在计算距离时遇到 NaN 会直接报错或产生完全无意义的模型。解决方法是特征提取后加一行检查np.isnan(X).any()有 NaN 就把对应样本找出来剔除。二是内存不足。当样本数很大几千张且特征维度很高上万维时SVM 需要计算样本两两之间的核函数值内存占用随样本量的平方增长。1000 个样本、12000 维特征核矩阵就有 100 万个数虽然 sklearn 做了优化但内存占用仍然可观。遇到这种情况解决方案是换 LinearSVC它对大规模样本的扩展性更好或者降维后再用 RBF 核。4.5 保存模型与标签映射训练一次后面直接调用训练完成并确认测试集分数满意后把模型、标签映射、以及特征提取时用的参数一起保存下来。这里有一个关键点很多人只保存了模型的权重没有保存标签映射和预处理参数结果预测新图片时要么标签对不上要么特征维度不一致。SVM 模型本身只存支持向量和系数但一个可用的预测系统需要把完整的处理链路都固化下来。import joblib # 保存模型、标签映射、特征提取配置 model_data { model: grid.best_estimator_, categories: categories, target_size: (160, 160), hog_params: { orientations: 8, pixels_per_cell: (8, 8), cells_per_block: (2, 2), }, hist_bins: 16, } joblib.dump(model_data, welding_svm_model.pkl) print(模型已保存到 welding_svm_model.pkl)joblib 是保存 sklearn 模型的标准方式它能高效处理大数组加载速度也比 pickle 快。把 categories 列表一并保存是必不可少的训练时标签是 0、1、2 这样的整数预测时拿到整数结果后需要把它映射回气孔夹渣这样的可读类别名。target_size 和 hog_params 也很重要新图片必须经过完全相同的预处理才能喂给模型否则特征分布不一致预测结果没有参考意义。保存完成后可以顺手打印出最优模型的支持向量个数。支持向量数量占总样本的比例能反映模型的复杂度比例太高比如超过 50%说明模型在用大量样本撑边界可能有欠拟合或数据重叠严重的问题比例很低则说明多数样本远离决策边界模型比较简洁。5. SVM 焊接缺陷识别避坑指南5 个让模型翻车的常见问题与排查5.1 训练集准确率接近满分但测试集惨不忍睹这是 SVM 图像分类里最常见的翻车现场。现象是训练集准确率高达 99%测试集准确率只有 60% 到 70%你甚至会怀疑是不是测试集标错了。原因通常是两个一是模型过拟合C 值太大或 gamma 值太大导致决策边界被少量样本牵着走二是训练集和测试集之间存在数据泄露比如同一种缺陷类型的不同角度图片被随机拆开分到了两边特征高度相似模型在测试时才会失效。排查方法很简单。先用网格搜索结果里的最优参数检查 C 和 gamma 是否落在取值边界上如果最优 C 是搜索范围的最大值 1000说明你给的搜索范围太小真正的解在更远处模型倾向于用高惩罚把每个样本都分对。再看数据划分方式如果数据集里本来就包含同一张图的多张连续帧用 train_test_split 随机划分就会泄露解决方案是按图片来源的父目录分组划分。注意网格搜索里如果最优参数刚好落在搜索范围的边缘就要扩大范围重跑不能直接把这个参数当作最终结果。这个规律对所有超参数搜索都成立。5.2 光照变化导致测试集表现不稳定焊接图片的光照条件是最难控制的因素。同一个气孔缺陷在不同曝光条件下拍出来灰度直方图分布完全不同但 HOG 特征受光照影响较小。现象是模型在某个批次的图片上准确率很高换一个批次就断崖式下降而你很难说清楚模型到底学的是缺陷本身的特征还是这张图的亮度特征。解决思路分三层。最底层的是特征层面在预处理阶段做直方图均衡化并把灰度直方图特征换成归一化直方图这样整体亮度变化不会改变直方图的形状只是平移了分布。中间层是数据层面训练集里尽量包含不同光照条件下的图片让 SVM 的支持向量覆盖更大的亮度变化范围。最上层是模型层面在网格搜索中同时测试 linear 核和 rbf 核线性核的泛化能力通常更强对光照变化的鲁棒性也更好。5.3 小类别样本太少导致分类器完全分不出这一类焊接缺陷数据集里最常见的分布是气孔和夹渣各有几百张样本裂纹和未熔合只有三四十张。现象是分类报告里大类别精确率 95%小类别精确率只有 20%模型几乎把所有样本都判成了大类别。根本原因是 SVM 的优化目标是整体准确率最大化小类别样本少即使全部错分对整体准确率的影响也不大。解决方法是加 class_weightbalanced前面第 4 章已经提到它会按类别样本数的反比调整权重让少数类别的错分代价变大。如果加了权重后小类别效果仍然不好就说明是小类别本身的特征和某个大类别的特征重叠严重这时候要做的是回看错误样本确认标注是不是有误或者针对这个类别补充更多的特征维度比如专门提取缺陷区域的形状特征并拼接到现有特征向量里。5.4 新图片预测时报特征维度不匹配这个错误很常见现象是你用训练好的模型预测一张新图sklearn 直接报维度错误大意是期望 12816 维但收到了 8000 维。原因是预测流程里图片的预处理参数和特征提取参数与训练时不一致最常见的是新图片没有缩放到 target_size或者直接调用了不同参数的 hog 函数。解决方法是把预测流程也封装成一个函数让它固定使用训练时保存的参数。你可以从保存的 pkl 文件里读回 target_size 和 hog_params然后在预测函数里传参调用。顺手写一个特征维度断言如果提取出来的特征长度不等于模型的特征维度直接报错并给出提示这样排查时一眼就能定位问题出在哪一步。5.5 PCA 降维在数据划分之前做导致数据泄露这条是 3.4 节提到的坑的展开。很多人为了省事先对整个数据集做 PCA 降维再划分训练集和测试集表面上看流程没问题实际却把测试集的信息泄露进了训练过程。因为 PCA 是在所有数据上拟合的训练阶段就已经见过测试集的主成分方向测试分数会被抬高。正确的做法是把 PCA 放进 pipeline 里让它在交叉验证的每一折中都重新拟合。sklearn 的 Pipeline 可以完美解决这个问题你只需要把降维器和分类器串起来交叉验证会自动保证每一折内部重新拟合 PCA。网格搜索时也要包在 pipeline 外面这样搜索到的参数才是最终部署时的真实表现。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.svm import SVC # 用Pipeline串联 标准化-降维-SVM避免数据泄露 pipe Pipeline([ (scaler, StandardScaler()), (pca, PCA(n_components0.95)), (svm, SVC(class_weightbalanced)), ]) # 网格搜索直接作用在pipeline上 param_grid { svm__C: [0.1, 1, 10], svm__gamma: [0.01, 0.1, scale], } grid GridSearchCV(pipe, param_grid, cv5, scoringf1_macro, n_jobs-1) grid.fit(X_train, y_train)Pipeline 的主要价值是让预处理-降维-分类成为一个不可分割的整体训练和预测时自动应用相同的操作顺序从根本杜绝了数据泄露和参数不一致这两类问题。建议从项目一开始就用 Pipeline不要先单独搞 PCA 再单独搞 SVC省得后面踩坑。6. 模型评估与导出用混淆矩阵和交叉验证验证模型再让它能独立运行训练完成不等于交付还要做两件事把模型的真实水平评估清楚以及让模型能脱离训练脚本独立预测新图片。from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt # 用最优模型预测测试集 best_model grid.best_estimator_ y_pred best_model.predict(X_test) # 混淆矩阵直观看到哪些类别互相混淆 cm confusion_matrix(y_test, y_pred) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelscategories) disp.plot(cmapBlues) plt.title(SVM焊接缺陷分类混淆矩阵) plt.savefig(confusion_matrix.png, dpi150)混淆矩阵是评估分类器最重要的工具它能看到准确率掩盖掉的细节——气孔被误判成夹渣是不是因为两者的灰度范围重叠裂纹被误判成咬边是不是因为形状特征不够明显。如果测试集上某个类别的混淆集中在一个方向回到特征提取阶段针对这个方向补充特征维度比继续调 SVM 参数更有效。交叉验证的评估同样重要。网格搜索里的 best_score_ 是五折交叉验证的平均 F1但这个分数是在训练集上算出来的只能代表模型在训练数据上的稳定程度。你要关注的是测试集上的准确率和 F1这才是模型面对没见过的数据时的真实表现。如果两者差距在 5 个百分点以内说明模型泛化能力正常差距超过 10 个百分点就要回到过拟合的排查流程里去。预测脚本是最后一块拼图。把预处理、特征提取、模型加载封装成一个类对外只暴露一个 predict 方法这样不管是单个图片文件还是批量目录调用逻辑都干净利落。import cv2 import numpy as np import joblib from skimage.feature import hog class WeldingDefectClassifier: def __init__(self, model_path): data joblib.load(model_path) self.model data[model] self.categories data[categories] self.target_size data[target_size] self.hog_params data[hog_params] self.hist_bins data[hist_bins] def predict_image(self, img_path): img cv2.imread(img_path, cv2.IMREAD_UNCHANGED) if len(img.shape) 3: img cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) img cv2.resize(img, self.target_size, interpolationcv2.INTER_AREA) img cv2.equalizeHist(img) img img.astype(float32) / 255.0 hist np.histogram(img, binsself.hist_bins, range(0, 1))[0] hist hist / hist.sum() hog_feat hog(img, **self.hog_params, feature_vectorTrue) features np.concatenate([hist, hog_feat]).reshape(1, -1) pred_idx self.model.predict(features)[0] return self.categories[pred_idx] # 单张图片预测 clf WeldingDefectClassifier(welding_svm_model.pkl) result clf.predict_image(./test_images/porosity_001.jpg) print(预测结果:, result)封装成类以后整个模型就像个黑盒输入图片路径输出类别名称。所有预处理细节都在类内部完成不会出现外部调用时忘记做直方图均衡化之类的问题。这个类也方便后续扩展如果你想加上返回每个类别的置信度功能只需要把 model.predict 改成 model.decision_function 或 predict_proba就能输出每个类别的得分。回顾这个完整流程预处理图片、提取特征、训练 SVM、调参、评估、封装预测从拿到数据集到产出可用的分类器核心代码不超过 200 行。相比深度学习方案这条路线的价值在于样本需求低、训练快、部署简单一台没有 GPU 的普通电脑就能撑起完整的开发和部署流程。我自己的习惯是每次拿到新的焊接缺陷数据都会先把不同特征组合的基线结果记录在一张表里——纯直方图、纯 HOG、直方图加 HOG、加 PCA 降维——然后集中调参数。这样做能快速判断瓶颈是特征还是模型而不是一上来就盲目调 C 和 gamma。最后再强调一遍焊接缺陷分类的成败七成在特征提取和预处理三成才在 SVM 本身。希望这个方法框架能帮你少走弯路尽快跑出一个稳定可用的焊接缺陷识别方案。本文还有配套的精品资源点击获取