二维二分类,看决策边界和支持向量
import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_moons from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC # 1. 生成数据 X, y = make_moons(n_samples=200, noise=0.25, random_state=42) # 2. 标准化 + RBF 核 SVM clf = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", C=1.0, gamma="scale")) ]) clf.fit(X, y) # 3. 画决策边界 xx, yy = np.meshgrid( np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 500), np.linspace(X[:, 1].min() - 1, X[:, 1].max() + 1, 500) ) Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape) plt.figure(figsize=(7, 5)) plt.contourf(xx, yy, Z, alpha=0.25, cmap="coolwarm") plt.scatter(X[:, 0], X[:, 1], c=y, cmap="coolwarm", edgecolors="k", s=30) # 4. 标出支持向量 sv = clf.named_steps["svm"].support_vectors_ # 注意:support_vectors_ 是标准化后的坐标,画图要逆变换回去 sv_original = clf.named_steps["scaler"].inverse_transform(sv) plt.scatter(sv_original[:, 0], sv_original[:, 1], s=120, facecolors="none", edgecolors="green", linewidths=1.5, label="支持向量") plt.legend() plt.title("RBF SVM 决策边界与支持向量") plt.show() print("支持向量数量:", len(clf.named_steps["svm"].support_vectors_)) print("训练集准确率:", clf.score(X, y))目的:直观理解“支持向量”到底是什么。
python from sklearn.datasets import make_moons from sklearn.svm import SVC import matplotlib.pyplot as plt import numpy as np X, y = make_moons(n_samples=200, noise=0.25, random_state=42) clf = SVC(kernel="rbf", C=1.0, gamma="scale").fit(X, y) # 画出决策边界和支持向量(完整代码见资料)📈 看图解释:
背景颜色分界:RBF核画出了一个弯曲的非线性边界。
绿圈圈(支持向量):只有离边界最近的那些点被圈起来了。如果在现实中,你把绿圈以外的点全部删掉,重新训练,画出来的分界线几乎一模一样。这就是支持向量的核心——决定边界的只是少数“关键人物”。
C 和 gamma 对决策边界的影响
import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import make_moons from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC X, y = make_moons(n_samples=200, noise=0.25, random_state=42) X = StandardScaler().fit_transform(X) xx, yy = np.meshgrid( np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 300), np.linspace(X[:, 1].min() - 1, X[:, 1].max() + 1, 300) ) grid = np.c_[xx.ravel(), yy.ravel()] fig, axes = plt.subplots(2, 3, figsize=(15, 9)) params = [ (0.1, 0.1), (1, 1), (100, 1), (1, 0.1), (1, 10), (100, 10), ] for ax, (C, gamma) in zip(axes.ravel(), params): clf = SVC(kernel="rbf", C=C, gamma=gamma) clf.fit(X, y) Z = clf.predict(grid).reshape(xx.shape) ax.contourf(xx, yy, Z, alpha=0.25, cmap="coolwarm") ax.scatter(X[:, 0], X[:, 1], c=y, cmap="coolwarm", edgecolors="k", s=15) ax.set_title(f"C={C}, gamma={gamma}, acc={clf.score(X,y):.2f}") plt.tight_layout() plt.show()用 2x3 的 6 张子图,直观感受什么是“过拟合”和“欠拟合”。
# 组合不同的 C 和 gamma,生成 6 张小图 params = [(0.1, 0.1), (1, 1), (100, 1), (1, 0.1), (1, 10), (100, 10)]左上看(C小,gamma小):边界极其平滑,像一条直线,这叫欠拟合。它连训练集都没分好。
右下看(C大,gamma大):边界像碎玻璃一样,围着每一个红蓝点绕圈,训练准确率极高,但遇到新数据肯定抓瞎,这叫严重过拟合。
看图结论:C 相当于“老师多严厉”(C越大越不能容忍分错),gamma 相当于“学生的视野半径”(gamma越大视野越窄,只认死理)。C和gamma必须配合着调,不能单看一个。
gamma 太大:边界围着每个点转,训练准确率高但很扭曲,过拟合;
gamma 太小:边界接近直线,欠拟合;
C 太大:容错少,边界紧贴异常点;
C 太小:容错多,边界平滑,可能欠拟合;
C 和 gamma 是交互的,不能单独看。
多分类 OvO vs OvR
SVM 原生只能分 2 类。如果要分 3 类(比如鸢尾花),sklearn底层是怎么做的?
OvO(一对一):3个类别,两两配对,训练
3*2/2 = 3个二分类器。如果类别有 100 个,就要训练 4950 个分类器,计算量大。OvR(一对多):3个类别,训练 3 个二分类器(A vs BC, B vs AC, C vs AB)。
注意:在
SVC里,decision_function_shape="ovr"只是把底层的 OvO 结果聚合成 OvR 的形状方便输出,底层训练依然是 OvO。
from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.multiclass import OneVsRestClassifier from sklearn.metrics import accuracy_score X, y = load_iris(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 1. 默认 SVC:底层 OvO ovo_clf = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", C=1, gamma="scale")) ]) ovo_clf.fit(X_train, y_train) ovo_pred = ovo_clf.predict(X_test) # 2. 显式 OvR:OneVsRestClassifier 包装 ovr_clf = Pipeline([ ("scaler", StandardScaler()), ("svm", OneVsRestClassifier(SVC(kernel="rbf", C=1, gamma="scale"))) ]) ovr_clf.fit(X_train, y_train) ovr_pred = ovr_clf.predict(X_test) print("OvO 准确率:", accuracy_score(y_test, ovo_pred)) print("OvR 准确率:", accuracy_score(y_test, ovr_pred)) # 查看底层训练了多少个二分类器 print("OvO 二分类器数量:", len(ovo_clf.named_steps["svm"].support_)) print("OvR 二分类器数量:", len(ovr_clf.named_steps["svm"].estimators_))网格搜索调 C 和 gamma
目的:用GridSearchCV自动找最佳参数。
param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": [0.001, 0.01, 0.1, 1, "scale"] }from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC X, y = load_iris(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) pipe = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf")) ]) param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": [0.001, 0.01, 0.1, 1, "scale"] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="accuracy", n_jobs=-1) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) print("交叉验证最优分:", grid.best_score_) print("测试集准确率:", grid.score(X_test, y_test)) # 看每个组合的 CV 分数 import pandas as pd res = pd.DataFrame(grid.cv_results_) print(res[["param_svm__C", "param_svm__gamma", "mean_test_score"]] .sort_values("mean_test_score", ascending=False).head())你跑出了前五名参数排名。发现
C=1.0, gamma=0.1是冠军,平均得分 0.98。为什么不同参数分数一样?因为鸢尾花数据集太简单了,只要参数在合理范围内(比如 C=10, gamma=0.1 和 C=1, gamma=0.1),都能分对,这体现了数据本身的鲁棒性。
线性核 vs RBF 核,高维稀疏场景
在处理高维稀疏数据(比如几万个词的文本)时,RBF 核计算量极大,容易过拟合。此时应该用LinearSVC(线性核)。
from sklearn.datasets import fetch_20newsgroups from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.svm import LinearSVC from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report # 只取两个类别,减少下载和训练时间 cats = ["rec.sport.baseball", "rec.sport.hockey"] train = fetch_20newsgroups(subset="train", categories=cats, remove=("headers", "footers", "quotes")) test = fetch_20newsgroups(subset="test", categories=cats, remove=("headers", "footers", "quotes")) clf = Pipeline([ ("tfidf", TfidfVectorizer(max_features=20000, stop_words="english")), ("svm", LinearSVC(C=1.0)) ]) clf.fit(train.data, train.target) pred = clf.predict(test.data) print(classification_report(test.target, pred, target_names=cats))进阶版
体验真实的工业级代码。使用Pipeline防止数据泄漏,开启probability=True输出置信度。
import numpy as np import pandas as pd from sklearn.datasets import load_breast_cancer # 换成一个更难的数据集 from sklearn.model_selection import train_test_split, GridSearchCV, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC # 1. 换数据集:乳腺癌预测(569个样本,30个特征,二分类) # 这个数据集比 Iris 难多了,更容易看出模型好坏 data = load_breast_cancer() X, y = data.data, data.target # 2. 划分数据 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 3. 搭建 Pipeline (注意这里加了 probability=True,为了后面输出概率) pipe = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", probability=True)) ]) # 4. 缩小网格搜索范围(数据集变了,原来的参数范围可能不是最优的) param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": [0.001, 0.01, 0.1, 1] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="accuracy", n_jobs=-1) grid.fit(X_train, y_train) # 5. 输出最优结果 best_model = grid.best_estimator_ print("=== 最佳参数 ===") print(grid.best_params_) print(f"测试集准确率:{best_model.score(X_test, y_test):.4f}") # ========================================== # 进阶玩法一:全局交叉验证(不再依赖单次随机划分) # ========================================== print("\n=== 进阶玩法一:用交叉验证评估模型 ===") # 用最优模型在整个训练集上做 5 折交叉验证 cv_scores = cross_val_score(best_model, X_train, y_train, cv=5) print(f"5次交叉验证的分数:{np.round(cv_scores, 4)}") print(f"平均分:{cv_scores.mean():.4f},标准差:{cv_scores.std():.4f}") # 注意:如果标准差很小,说明模型非常稳定;如果标准差大,说明数据某些划分太极端。 # ========================================== # 进阶玩法二:输出概率 (predict_proba) # ========================================== print("\n=== 进阶玩法二:输出概率(软判决) ===") # 我们挑测试集的前 5 个样本看看 probas = best_model.predict_proba(X_test[:5]) preds = best_model.predict(X_test[:5]) for i in range(5): # probas[i][0] 是恶性概率,probas[i][1] 是良性概率 print(f"样本 {i+1}: 真实标签={y_test[i]}, 预测标签={preds[i]}, " f"属于恶性的概率={probas[i][0]:.4f}, 属于良性的概率={probas[i][1]:.4f}") # ========================================== # 进阶玩法三:查看 20 个参数组合的排名表 # ========================================== print("\n=== 进阶玩法三:参数排名 ===") res = pd.DataFrame(grid.cv_results_) top_results = res[["param_svm__C", "param_svm__gamma", "mean_test_score"]].sort_values("mean_test_score", ascending=False) print(top_results.head(5).to_string(index=False))输出结果:
5次交叉验证的分数:[0.967 1. 0.956 0.989 0.989]
平均分:0.9802,标准差:0.0162=== 进阶玩法二:输出概率(软判决) ===
样本 1: 真实标签=0, 预测标签=0, 属于恶性的概率=1.0000, 属于良性的概率=0.0000
样本 2: 真实标签=1, 预测标签=1, 属于恶性的概率=0.0000, 属于良性的概率=1.0000
样本 3: 真实标签=0, 预测标签=0, 属于恶性的概率=0.9897, 属于良性的概率=0.0103
样本 4: 真实标签=1, 预测标签=1, 属于恶性的概率=0.4793, 属于良性的概率=0.5207
样本 5: 真实标签=0, 预测标签=0, 属于恶性的概率=1.0000, 属于良性的概率=0.0000=== 进阶玩法三:参数排名 ===
param_svm__C param_svm__gamma mean_test_score
10.0 0.010 0.980220
100.0 0.001 0.978022
10.0 0.001 0.973626
1.0 0.010 0.969231
100.0 0.010 0.969231
第一部分:代码到底在干嘛?(我们的“AI医生培养计划”)
换教材:之前咱们用 Iris 鸢尾花(4个特征),闭着眼睛都能分对。现在换成
load_breast_cancer(乳腺癌数据,30个特征,包含肿瘤大小、纹理等)。这个难度高得多,更接近真实世界。分考场:代码里的
train_test_split把 569 个病人的数据,抽 80% 给 AI 当“复习资料”(训练集),留 20% 藏起来当“期末考试”(测试集)。建流水线 (Pipeline):
StandardScaler负责把数据拉平(标准化),SVC是 AI 医生的大脑。重点:代码里加了
probability=True。以前的 AI 医生只会喊“是恶性!”或者“是良性!”,加了这句,AI 医生会告诉你:“我有 90% 的把握是恶性”。
海选教练 (GridSearchCV):咱们给了 AI 医生 16 种不同的“学习态度”(C 和 gamma 的 4x4 组合),让它在复习资料里自己模拟考(交叉验证),挑出考得最好的那个态度。
第二部分:结果一,为什么有 5 个分数?(AI的模拟考成绩)
5次交叉验证的分数:[0.967 1. 0.956 0.989 0.989],平均分:0.9802,标准差:0.0162
我们在代码里写了cross_val_score,这相当于把复习资料平分成了 5 份。
第 1 次:用第 1 份当卷子,剩下 4 份复习。考了 0.967(96.7分)。
第 2 次:用第 2 份当卷子,剩下 4 份复习。考了 1.0(满分!说明那份卷子里的病人特征太典型了)。
以此类推,考了 5 次。
为什么看标准差?标准差 0.0162 非常小。意思是,不管拿哪份卷子考,AI 医生都能稳定在 95% 以上。在医疗里,稳定比偶尔考满分重要一万倍!
第三部分:结果二,概率那堆数字怎么看?(AI医生在犹豫什么?)
样本 1:真实标签=0(恶性),预测标签=0(恶性),属于恶性的概率=1.0000
翻译:AI 医生看了一眼样本 1,拍着胸脯说:“这绝对是恶性肿瘤,我有 100% 的把握!”(结果确实对了)。
重点看样本 4:真实标签=1(良性),预测标签=1(良性),恶性概率=0.4793,良性概率=0.5207
翻译:这是整段结果里最有价值的一个数据!
AI 医生看了一眼样本 4,挠了挠头说:“这个肿瘤长得……有点怪。我觉得它有 52% 的概率是良性,但它也有 48% 的概率是恶性。我最终判它良性,但我心里非常没底。”
这就是
probability=True的威力:
如果只看预测标签,医生以为 AI 很有把握。但看概率,医生会发现:“哎哟,48% vs 52%,这太接近了!这属于极度危险的临界样本!”于是,医生会立刻把这个病人拉回来,重新做个切片化验。这就是概率输出在工业界(尤其是风控、医疗)最大的作用:帮你抓出那些“模型不确定”的疑难杂症。
第四部分:结果三,参数排名为什么大变样?
现在的冠军:
C=10.0, gamma=0.01
(以前 Iris 数据集的冠军:C=1.0, gamma=0.1)
你肯定想问:为什么换了个数据集,冠军参数就变了?
维度变高了:Iris 只有 4 个特征,乳腺癌有 30 个特征。特征一多,数据在空间里的距离就拉大了。
gamma 必须变小(0.1 -> 0.01):gamma 控制每个样本的影响力范围。特征越多,影响力越容易被稀释。所以 gamma 变小,相当于把 AI 医生的“探照灯”打得更宽一点,让它能统观全局。
C 必须变大(1 -> 10):C 越大,说明模型对错误越不能容忍。面对复杂的乳腺癌数据,稍微严厉一点(C=10),反而能让边界刻画得更精准。
SVM 优缺点总结
优点:
数学理论极其完美(凸二次规划,有全局最优解)。
小样本、高维数据表现极佳。
支持向量决定了模型,模型体积小,泛化能力强。
缺点:
大样本计算极其缓慢(O(n2)O(n2))。
对缺失数据和特征缩放(标准化)极度敏感。
多分类处理起来较麻烦(OvO计算量大)。
需要精心调参(C、gamma、核函数)。
代码避坑指南
只要是 SVM,不管你用什么核,数据必须标准化(StandardScaler)!如果特征里有 0.01 和 10000,距离计算直接崩溃。
标准化必须写在
Pipeline里面,或者只用训练集的均值和方差去fit,绝对不能用全部数据去fit然后做交叉验证(这叫数据泄露)。不要一上来就调大样本的核 SVM,先试试
LinearSVC跑个 baseline。