简介:基于支持向量机(SVM)的人体背部曲线分类识别MATLAB实现,面向机器学习、图像处理和医学形态分析方向的学习者,用于根据背部轮廓图像自动完成不同姿态或类别的判别。压缩包共114个文件,约1.64MB,核心资源为110张JPG人体背部样本图片和2个MATLAB源程序(.m),另有2个ASV自动备份文件;图片按不同背部形态编号排列,源程序涵盖图像读取、特征提取、SVM训练与分类识别的完整流程。从预览可看出工程包含主程序入口和多张人体系列样本图,数据与代码组织清晰,方便直接定位。代码带详细注释,运行主脚本即可复现分类结果,并且能观察不同类别背部曲线的特征差异。目前已有80人学习浏览,适合课程设计、毕业设计、SVM入门实践或进一步扩展研究,也便于替换图片或调整核函数、惩罚参数进行算法对比和二次开发。
1. 人体背部曲线分类识别:为什么SVM是比深度学习更稳的起点
脊柱侧弯筛查和体态评估里,“人体背部曲线”是核心观察对象。医生用手指沿棘突划线,靠目测判断有没有弯、弯到什么程度,经验不同结果就差很多;照片测量又容易受角度和光线影响。用程序做基于支持向量机SVM的人体背部曲线分类识别,本质就是把背部的几何轮廓变成一组数值特征,再交给SVM分类器区分正常、轻度侧弯和重度侧弯。我见过不少团队一上来就上CNN,手里样本只有两三百条,网络还没收敛就开始过拟合,调参调得怀疑人生;反而SVM在几百条小样本上稳定跑出不错准确率,训练按秒算,参数也就核函数、C、gamma几个,路径清晰。这篇文章给正在做体态评估项目、手里已经有曲线数据或能采集到数据的工程师,按数据整理、特征设计、SVM选型、代码实现、避坑排查的顺序讲完整个落地过程。
2. 背部曲线数据怎么整理:关键点采样、特征设计与标注规范
2.1 背部曲线的数据来源与存储格式:点云、轮廓图还是关键点坐标
背部曲线数据常见的获取方式有三种。深度相机(结构光或双目方案)能直接输出背部点云,从中提取脊柱正中线,采集成一系列三维点;普通照片经过人体分割,也能得到背部轮廓和脊柱棘突的连线;还有一类体态评估设备直接输出关键点坐标数组,颈椎C7、胸椎各节到骶椎S1都已经标好。不管原始来源是哪种,落到SVM输入之前,最好统一成关键点坐标序列:从颈部到腰部等间隔取N个点,每个点一个(x, y)坐标,存成CSV,一行是一条样本。
常见的数据组织方式是这样的:CSV第一行是列名,形如C7_x,C7_y,T1_x,T1_y,...,label,后面每行是一条人背的测量结果。坐标单位统一用毫米或厘米,label是类别编码。这里有一个容易被忽略的约定:关键点顺序必须一致,要么都从颈椎往骶椎排,要么反过来,绝对不能某些样本正序、某些样本倒序。我一般会在读取后立刻做一次顺序校验,比如计算相邻点距离的方差,方差异常大就说明有样本的坐标列顺序错了。
样本量方面,按我的经验,SVM在小样本上表现稳定,但也不是越少越好。每个类别最好不少于50条,三个类别合计150到300条是甜区;少于这个量,特征稍微多一点就容易抖。如果手里只有几十条,优先考虑用线性核而不是RBF核,后面会解释原因。数据集的划分比例我习惯用75%训练、25%测试,并且切分时强制按类别比例分层,避免某一类全挤进测试集。
2.2 曲线特征提取:偏移量、曲率、侧偏角与不对称系数
拿到原始坐标后,不能直接把坐标拉平当特征喂给SVM。SVM对特征维度没有序列概念,把N个点的x、y坐标依次展开成2N维向量,等价于把每个坐标当作独立维度,曲线相邻点之间的关系全丢了。正确做法是先把坐标转换成“几何上有含义”的特征,让分类器学到的是弯的形状,而不是一堆散点。
我经常用四类特征组合。第一类是偏移量:取首尾点连线作为竖直参考线,计算每个关键点相对这条参考线的水平偏移。正常背部偏移量基本在零附近小范围波动;C形侧弯会呈现单向偏移递增;S形侧弯则是一段正向偏移接一段负向偏移,正负交替的位置对应弯曲拐点。第二类是曲率:用相邻三点夹角的补角衡量局部弯曲程度,夹角越“折”,曲率值越大。第三类是整体侧偏角:计算首尾点连线和竖直方向的夹角,反映整条脊柱的倾斜程度。第四类是上下半段的面积不对称系数:分别累加上半段、下半段相对参考线的偏移面积,取差值比,用来区分弯在上段还是下段。
这四类特征组合起来维度不高。假设取10个关键点,偏移量10维,曲率8维,加上侧偏角和不对称系数各1维,总共20维。两百条样本、20维特征,这个比例对SVM非常友好,不需要额外做PCA降维。特征之间的物理单位不同,偏移量是毫米量级、曲率是无量纲的小数、角度是弧度,所以后面必须做标准化,这一步忘掉的翻车率极高,后面避坑章细说。
2.3 样本标注与数据集划分:先定标准再跑模型
标注是整条链路里最影响结果的一环,而且经常被低估。类别定义最好在采集之前就用书面标准定死:0代表正常,1代表轻度,2代表重度;具体的判定依据可以是Cobb角阈值、医生临床分级,或者是你们设备和金标准对比出来的结果。切忌“看着像轻度就标1”,同一个采集批次最好由同一个标注人来完成,或者至少统一标注口径,否则类别边界本身就是模糊的,再好的SVM也学不出清晰间隔。
数据集切分有个关键点:要用stratify保住类别比例。假设重度类别只占10%,如果不做分层切分,随机抽样后测试集里重度样本可能只有几条甚至没有,评估结果波动很大。代码上就是train_test_split里加一个stratify=y参数,一行解决。还有一点:如果数据是从不同设备、不同批次采集的,切分时最好按批次分组而不是纯随机,避免同一设备的特有噪声同时混进训练集和测试集,把设备差异误当成分类能力。
3. SVM在该分类任务里的选型逻辑:核函数、C与gamma的直观理解
3.1 为什么小样本曲线分类任务选SVM而不是神经网络
背部曲线分类这类任务有个典型特点:样本量小、特征维度低、类别相对清晰。神经网络在这个区间赢不了SVM。小样本下CNN很容易把训练集背下来,验证集却不见涨;而SVM的间隔最大化本质是在控制结构风险,也就是说它在拟合训练数据的同时,还在约束决策边界的复杂度,天然带正则化效果。用二三百条曲线训练一个ResNet是很勉强的事,但训一个RBF核SVM绰绰有余。
另外,SVM的复现性比神经网络好得多。深度学习跑一次一个结果,还要盯随机种子和初始化;SVM是一个凸优化问题,同一份数据、同一组参数,每次跑出来的结果一致,这对医疗相关场景很重要——你要给别人写报告、讲清楚怎么复现,SVM省很多解释成本。训练时间差异更明显,CPU上几秒钟的事,根本用不上GPU。
还有一个常被忽视的优点:SVM在特征维度不高时依然表现稳定。20维特征配上两百个样本,SVM不需要额外做特征筛选,而神经网络在这个维度下很容易把无关特征的噪声也学进去。所以我的选型判断是:样本量低于500、特征维度低于100,先别上深度学习,拿SVM跑一版基线,大概率结果够用。
3.2 RBF核和线性核的边界:gamma参数在曲线特征上意味着什么
核函数的选择直接决定决策边界的形状。线性核就是原始特征空间里画一条超平面,参数只有C,计算最快、最不容易过拟合,但前提是数据基本线性可分。背部曲线的特征里,偏移量和曲率跟侧弯程度往往是单调相关的,不少情况下线性核就能出不错的效果。我建议第一次跑通流程时先用线性核,拿到一个基线准确率。
RBF核则把样本隐式映射到更高维空间,能拟合更复杂的边界。它有两个参数:C和gamma。gamma直观理解是“单条样本的影响半径”:gamma越小,每条样本只影响邻近区域,决策边界越平滑;gamma越大,每个样本都撑起一个尖峰,边界跟着局部细节走,很容易把噪声也学进去。在曲线特征上,如果你发现训练集准确率接近100%、测试集差一截,第一步就去看gamma是不是设太大了。RBF的gamma取值跟特征尺度也有关,所以特征标准化这步不能省;标准化之后,我从0.01到1的范围开始扫。
核函数选择的另一个考量是:线性核只有一个超参数,RBF有两个,网格搜索的组合数翻好几倍。样本量特别少的时候,我一般用线性核或者gamma很小的RBF,避免模型在稀疏区域乱画边界。
3.3 C与class_weight:硬间隔和类别倾斜之间的平衡
SVM的损失函数由间隔和误分类惩罚两部分组成,C就是两者之间的权重。C大,对误分类惩罚重,模型宁可把边界画得曲折也要把训练集分对,但测试集容易翻车;C小,允许一定程度的误分类,边界更平滑,泛化通常会好一些。调参路径上,先固定一个gamma,把C从0.1到100按数量级扫一遍,看验证集表现,比两个参数一起乱试要容易定位问题。
class_weight这条值得单独说。背部曲线数据里重度样本天然比正常样本少,尤其是去医院场景下,正常样本可能占70%,重度只占5%。如果不做处理,SVM为了整体准确率,倾向于把所有不确定样本都判成多数类,结果是重度这个最需要被发现的类别recall接近0。解决办法有两个:一是class_weight="balanced",让Sklearn按类别频率自动加权;二是对少数类别做重采样,但曲线数据扩增要小心,插值造出来的曲线不能太假。我一般优先用class_weight,参数简单,效果也稳定;若类别比例极端到10比1以内,再考虑SMOTE之类的过采样手段。
4. 跑通SVM分类:数据加载、特征提取、训练评估的完整代码与参数说明
4.1 数据加载与检查:列名、缺失值和关键点顺序校验
开始写代码前,先确认数据是哪种形态。如果已经有人帮你提取好了特征文件,每行是特征向量加label,那直接跳到4.3;如果手头还是原始坐标点,就需要自己写特征提取。下面这段代码假设CSV里存的是坐标序列,列名严格成对排列,label在最后一列。
import numpy as np import pandas as pd # 读取原始曲线坐标数据 # 列名约定:C7_x, C7_y, T1_x, T1_y, ..., S1_x, S1_y, label df = pd.read_csv("back_curve_samples.csv") print("样本形状:", df.shape) print("类别分布:", df["label"].value_counts().to_dict()) # 第一步先查缺失值和无效值:SVM对NaN零容忍 assert not df.isnull().values.any(), "存在NaN,需要先补全或删除该样本" assert np.isfinite(df.iloc[:, :-1].values).all(), "存在Inf值,需要清洗" # 第二步校验关键点数量:坐标列数减半就是关键点个数 n_points = (df.shape[1] - 1) // 2 print("关键点数量:", n_points)这段代码做了三件事:确认样本量和类别分布、排除NaN和Inf、算出关键点数量。类别分布这一行输出很关键,如果某一类样本数量少到个位数,后面训练出来的模型没有参考价值,不如回头先补数据。n_points这个值后面要用,特征提取函数需要知道每条样本有多少个点。
需要注意列名必须严格成对。如果CSV里还有ID列、采集日期列,一定要先剔除,否则df.shape[1] - 1算出来的点数就是错的,特征提取时reshape(-1, 2)会直接报错。我见过不止一次的翻车现场是:多加了一列时间戳,坐标被reshape成一串错位数组,特征值异常离谱,SVM准确率还不如随机猜。
4.2 特征提取:从坐标序列到几何特征向量
拿到坐标以后,把它们从DataFrame里拆出来,逐条转换成特征向量。这里给出完整的特征提取函数,整合了前面说的偏移量、曲率、侧偏角、不对称系数四类特征。
def extract_features(points): """ 从背部曲线关键点坐标提取特征向量。 points: (n_points, 2) 数组,顺序固定为从颈椎到骶椎。 返回: 1维特征数组,顺序为 offsets + curvatures + [tilt, asymmetry] """ points = np.asarray(points, dtype=np.float64) n = len(points) # 1. 偏移量:每个点相对首尾连线的水平偏移 # 用linspace在首尾点x坐标之间均匀插值,作为竖直参考线 ref_x = np.linspace(points[0, 0], points[-1, 0], n) offsets = points[:, 0] - ref_x # 2. 曲率近似值:相邻三点夹角的补角,夹角越折,值越大 curvatures = [] for i in range(1, n - 1): v1 = points[i] - points[i - 1] v2 = points[i + 1] - points[i] cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-8) curvatures.append(1.0 - cos_angle) # 3. 整体侧偏角:首尾连线相对竖直方向的角度,弧度制 end_vector = points[-1] - points[0] tilt = np.arctan2(end_vector[0], end_vector[1]) # 4. 上下半段的面积不对称系数 half = n // 2 area_up = np.abs(np.sum(points[:half, 0] - ref_x[:half])) area_down = np.abs(np.sum(points[half:, 0] - ref_x[half:])) asymmetry = (area_up - area_down) / (area_up + area_down + 1e-8) return np.concatenate([offsets, curvatures, [tilt, asymmetry]]) # 把整个DataFrame转成特征矩阵 X = np.vstack([ extract_features(df.iloc[i].drop(columns=["label"]).values.reshape(-1, 2)) for i in range(len(df)) ]) y = df["label"].values print("特征矩阵形状:", X.shape)这个函数输出特征维度是n_points + (n_points - 2) + 2。10个关键点时就是10加8加2等于20维。offset部分直接反映曲线的横向偏移轨迹,是SVM最重要的输入;curvature负责刻画局部拐弯的剧烈程度;tilt是一个标量,对整体歪斜敏感;asymmetry则帮助区分弯的位置在胸段还是腰段。
几个实现细节说明。计算夹角时给分母加了1e-8,防止两个点重合时除零;同一批数据的n_points必须一致,如果有样本点少了几个,SVM会因为特征维度不一致直接拒绝训练;np.vstack逐条调用函数,数据量几百条时完全够用,不必过度优化性能。另外,如果头部或尾部的关键点质量不可靠,可以适当裁剪或加权重,但要在所有样本上统一操作。
4.3 训练SVM分类器:先切分再归一化,Pipeline串起来
特征矩阵准备好后进入训练环节。这里最容易犯的错误是:先对整个X做标准化,再切分训练集和测试集。正确顺序是先切分,再用训练集拟合标准化器,测试集只能用同一个scaler做transform,否则测试集的信息提前渗入训练过程,测出来的准确率是虚高的。用Pipeline可以自动规避这个问题。
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.pipeline import Pipeline # 切分训练测试集:分层抽样,保住类别比例 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.25, random_state=42, stratify=y ) # Pipeline串起标准化与SVM,fit时先scale再训练,predict时自动做同样的scale model = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC( kernel="rbf", C=1.0, gamma=0.1, class_weight="balanced", random_state=42 )) ]) model.fit(X_train, y_train) print("训练集准确率: {:.4f}".format(model.score(X_train, y_train))) print("测试集准确率: {:.4f}".format(model.score(X_test, y_test)))参数含义拆开讲。kernel="rbf"是当前默认选择,适合曲线特征非线性可分的情况;C=1.0是误分类惩罚的初始值,先按默认跑通再调;gamma=0.1是RBF核的影响半径,在标准化后的特征上是一个相对保守的数值;class_weight="balanced"让模型自动按类别频率加权,重度样本少也不至于被淹没。random_state=42是固定随机种子,保证每次跑出的结果一致。
Pipeline在这里的好处是:fit时先对训练集计算均值和方差,再用同一套均值和方差变换测试集;你不需要手动记住scaler的状态。如果训练集和测试集准确率差距超过15个百分点,不用怀疑,要么gamma太大,要么特征里有离群值,先去查数据而不是继续调参。
4.4 评估要看什么:准确率、precision与recall的分歧
准确率只能告诉你整体对不对,在类别不平衡时容易骗人。假设正常类占70%、重度占5%,模型把所有样本都判成正常,准确率也有70%,但重度一个没查出来,这个模型在体检场景里等于废的。所以分类报告要逐类看precision和recall。
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test) # 按业务类别名称输出分类报告 print(classification_report(y_test, y_pred, target_names=["正常", "轻度", "重度"])) # 混淆矩阵,行列分别对应真实类别和预测类别 cm = confusion_matrix(y_test, y_pred) print("混淆矩阵:\n", cm)读报告的顺序有讲究。先看每个类的recall:正常类recall低于90%,模型误伤太多,筛查场景会产生大量转诊;重度类recall低于70%,漏诊风险高,说明class_weight还不够或者特征区分度不足。再看precision:重度类precision低,说明模型把不少轻度样本错判成重度,系统可以再优化但不算致命。混淆矩阵能告诉你错在哪一对类别之间——如果大部分错误是“轻度→重度”双向混淆,说明这两类的特征边界本身就模糊,可以回去检查标注标准;如果错误集中在“正常→轻度”,则可能是阈值偏向问题,可以调整决策阈值而不是重新训练。
这一版的SVM训练流程已经具备完整的可运行性。接下来要回答的问题是:这些参数是默认值,怎么找到更稳的组合、避免掉进常见的坑。下面两章分别讲排查和调优。
5. 踩坑与排查:从特征泄漏到类别不平衡的5个实际问题
5.1 归一化漏掉之后:模型准确率卡在50%上下
现象:代码流程跟正常训练一模一样,但训练集准确率也只有60%不到,测试集更差,无论怎么调C和gamma都上不去。
原因:特征矩阵里偏移量是毫米量级,数值可能到几十;曲率是零点几的小数;侧偏角是弧度;不对称系数接近零。SVM的间隔计算依赖样本点的距离,量纲大的维度直接主导距离,量纲小的维度即便区分度再高也发挥不出来。漏掉StandardScaler的情况下,模型实际上是在拿偏移量的绝对值硬分类,其他特征等于噪声。
解决:把标准化加进Pipeline,并且确保它是第一条。不要手动单独对X做scaler.fit_transform(X)再切分,那样会让测试集的信息通过均值和方差渗透进训练,属于数据泄漏;在Pipeline里,scaler只会在训练集上fit,这是Sklearn推荐的标准做法。加了标准化之后,C和gamma这两个参数的取值范围才算有意义。
5.2 类别不平衡:重度样本recall为0,模型却在“装死”
现象:分类报告里正常类recall高达95%,轻度类也还行,但重度类recall是0.00,一条都没查出来。整体准确率看着不错,实际漏掉的全是最需要发现的样本。
原因:SVM默认目标是最小化整体误分类数,重度类样本少,分错了对整体损失影响小,模型自然倾向于把所有边界附近的样本都判给多数类。这在医疗筛查场景是致命的:你可以接受正常人多跑一趟复查,但接受不了重度侧弯被漏掉。
解决:先看类别分布,确认不平衡程度。然后给SVC加class_weight="balanced",让少数类的误分类代价自动抬高。加了之后重度类recall通常会有明显改善,但要注意正常类precision可能略微下降,这是正常的权衡。如果类别比例悬殊到重度样本只有十几条,光靠class_weight不够,还要考虑对重度类做重采样或者在分类决策时手动降低阈值,让更多模糊样本被判成重度,再由医生复核。
5.3 盲目用RBF核:训练集100%但测试集只有60%
现象:训练集准确率接近100%,测试集掉到60%左右,调小C也没有明显改观。
原因:这是过拟合的典型信号,根源多半在gamma。gamma取值过大时,RBF核的决策边界完全跟着训练样本走,每个样本周围都形成了独立的影响区域,模型把训练集背了下来,但换一批新样本就露馅。注意这是RBF核特有的问题,线性核没有gamma参数,反而更抗过拟合。
解决:把gamma往小了调,比如从1.0降到0.01,看验证集表现。比较可靠的做法是不要凭感觉定,直接用网格搜索在C=[0.1, 1, 10, 100]和gamma=[0.001, 0.01, 0.1, 1]的范围内交叉验证。另外,如果样本量只有百来条,直接换线性核更省心,线性核在这个数据规模下通常已经够强,而且少一个参数少一堆玄学调参。
5.4 先归一化再切分:交叉验证分数虚高的数据泄漏
现象:代码里先写了scaler.fit_transform(X),然后才train_test_split,结果测试集准确率异常高,高到让你觉得模型完美了。但换一份新数据来测,立刻打回原形。
原因:顺序写反了。StandardScaler在全体数据上计算均值和方差时,已经把测试集的信息混进去了;模型训练时看到的“分布范围”提前知道了测试集的长相,测试集不再是无偏评估。这不是SVM的bug,是评估流程的常见数据泄漏。
解决:把切分放在最前面,或者直接用Pipeline。我习惯是:数据读到内存,先检查清洗,然后立刻切分,再谈归一化和特征变换。train_test_split产生的X_train, X_test,任何数据变换都只能在X_train上fit。用Pipeline的好处是不用记这些顺序,它天然保证这个纪律。
5.5 关键点顺序不一致:特征向量出现极端离群值
现象:特征矩阵里有若干行的数值比其他样本大几个数量级,比如偏移量动辄上千;SVM训练过程中警告有样本远离决策边界,准确率时好时坏。
原因:数据采集时某些样本的坐标列顺序是反的,或者中间漏了点位。extract_features把首点当颈椎、末点当骶椎,如果一段样本恰好是反的,points[-1] - points[0]算出来的首尾连线方向就反了,后续所有偏移量计算全部错乱。
解决:写一个校验步骤,计算每条样本相邻点距离的方差。正常背部曲线相邻点距应该比较均匀,方差在合理范围内;若某条样本的方差是平均值的几十倍,多半是点位顺序问题。另一种校验方法是看首尾点坐标:颈椎C7和骶椎S1在x方向通常居中、y方向差最大,如果首点y坐标反而最小,就翻转数组。这个坑在手工标注的数据里出现频率很高,脚本校验比肉眼检查可靠得多。
6. 参数调优与结果验证:网格搜索、混淆矩阵与新样本预测
6.1 用网格搜索稳定复现最优参数组合
手动调C和gamma很容易陷入局部满意,换一组随机种子结果就不稳。网格搜索配合交叉验证是更可靠的做法:把参数空间铺开,每个组合都训练K轮,取平均分比较。
from sklearn.model_selection import GridSearchCV from sklearn.svm import SVC from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler model = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", class_weight="balanced", random_state=42)) ]) param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": [0.001, 0.01, 0.1, 1], } grid = GridSearchCV( model, param_grid, cv=5, scoring="f1_macro", n_jobs=-1 ) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) print("交叉验证最优F1:", grid.best_score_)这里有几个关键选择。scoring="f1_macro"而不是accuracy,因为类别不平衡时accuracy会把多数类的主导地位带入评分中,f1_macro对每个类一视同仁,更能体现少数类的分类能力。cv=5表示5折交叉验证,样本量少时这个折数合适,更少可能方差大,更多训练成本高。网格里的C和gamma各取4个值,组合16组,每组训练5轮,总共80次SVM训练,数据量只有几百条时几秒钟就跑完。n_jobs=-1表示用满CPU核心,跑完记得检查grid.best_params_,如果最优值落在网格边界上,说明搜索范围没包住,需要扩展该方向的取值范围重跑一次。
6.2 对单条新曲线做预测:从坐标到分类结果的完整流程
模型调好后,实际使用场景是一条新的人体背部曲线进来,快速给出分类结果。因为用的是Pipeline,predict时自动完成标准化,不需要手动处理一遍scaler。
def predict_curve(points): """ 输入一条背部曲线的关键点坐标,输出分类结果和决策函数分数。 points: (n_points, 2) 数组。 """ feats = extract_features(points).reshape(1, -1) label = grid.best_estimator_.predict(feats)[0] score = grid.best_estimator_.decision_function(feats)[0] return label, score # 示例:新样本坐标(11个关键点,坐标顺序从颈椎到骶椎) new_points = np.array([ [2.1, 0.0], [1.9, 1.0], [1.8, 2.0], [2.3, 3.0], [3.1, 4.0], [3.8, 5.0], [4.2, 6.0], [4.0, 7.0], [3.5, 8.0], [3.0, 9.0], [2.8, 10.0] ]) label, score = predict_curve(new_points) print("预测类别:", ["正常", "轻度", "重度"][label]) print("决策函数分数:", score)这里用decision_function而不是predict_proba是有原因的。SVC默认不输出概率,改成probability=True需要额外做Platt缩放,训练时间增加,且小样本上概率校准并不一定可靠。decision_function返回的是“一对多”模式下各个类别的决策得分数值,最大分数对应的类别就是最终预测,分数绝对值可以当作置信度的粗略参考——离零点越远,把握越大,但别把它当天花板。
6.3 用PCA可视化验证特征可分性,而不是只盯准确率
调参调到最后,准确率已经能看的时候,建议花两分钟做一次PCA可视化。把特征降到二维,按类别染色画在散点图上,能直接看到这张分类问题的天花板在什么位置。
from sklearn.decomposition import PCA import matplotlib.pyplot as plt pca = PCA(n_components=2) X_pca = pca.fit_transform(X_test) colors = ["green", "orange", "red"] labels = ["正常", "轻度", "重度"] for i in range(3): mask = (y_test == i) plt.scatter(X_pca[mask, 0], X_pca[mask, 1], c=colors[i], label=labels[i], edgecolor="k", s=40) plt.xlabel("PC1") plt.ylabel("PC2") plt.legend() plt.show()一个血泪经验:如果PCA图上三个类别几乎完全重叠,问题不在SVM,也不在C和gamma,而在特征本身。这时候继续调参只是自我安慰,应该回头重新设计特征——比如增加更多关键点、加入弯曲角度的分段统计,而不是跟核函数较劲。如果图上类别边界清晰但线性不可分,RBF核就有发挥空间;如果边界清晰且近乎线性,线性核反而更稳。可视化不能替代量化评估,但它能告诉你还有多少提升空间,以及该往哪个方向努力。
这类任务我做完之后的习惯是:把训练好的Pipeline、特征提取函数和PCA可视化代码一并保存,参数记录在项目说明里。两周后有人问起“这个重度样本为什么分出来了”,还能从特征和决策分数里把原因讲清楚。SVM这条路,数据整理了、特征做对了、参数不贪多,大多数时候都能稳定落地。希望帮到你。
本文还有配套的精品资源,点击获取