45 · SVM 支持向量机:最大间隔分类器
🔗 知识图谱导航:阅读本文前,建议先回顾《40 · 线性与逻辑回归》中的二分类与正则化概念,以及《41 · 分类树与随机森林》中的泛化评估思路;本文会重点解释 SVM 如何通过“最大间隔”提升分类稳定性。
运行环境:
pip install numpy scikit-learn。本文使用 sklearn 生成 Mock 分类数据,不需要下载外部数据集。
痛点与架构:逻辑回归只要找到一条能分开的线,SVM 追求的是“离两边样本都尽量远”的线。这个最大间隔思想,让 SVM 在高维文本、图像特征等场景里很有代表性。本文先看线性间隔,再看核函数如何处理非线性边界。
SVM 先建立直觉
支持向量:离分界线最近、真正决定边界的样本点。
最大间隔:让分界线离两类最近样本都尽可能远。
核函数:不手动升维,也能让模型处理弯曲边界。
极客解析:SVM 像在两群人之间修隔离带。普通分类器只要能隔开就行,SVM 要让隔离带尽量宽;边界由离隔离带最近的几个人决定,他们就是“支持向量”。
步步为营:核心逻辑自适应拆解
这一篇拆成 6 个台阶:数据标准化、参数表、线性核、核函数对比、ASCII 决策边界和 CLI 调度。每段都能独立运行并打印结果。
Step 1:用 make_data 生成并标准化 SVM 训练数据
痛点与机制:
make_data 负责造出分类数据、切分训练集/测试集,并做标准化。SVM 对特征尺度很敏感,就像不同单位的尺子不能直接比:收入用万元、年龄用岁,如果不标准化,距离和间隔会被大数值特征带偏。
核心源码(逐字来自文末完整源码):
def make_data(n_samples: int = 600) -> Tuple[np.ndarray, np.ndarray,
np.ndarray, np.ndarray]:
"""生成标准分类数据集,标准化后返回。"""
X, y = make_classification(
n_samples=n_samples, n_features=10, n_informative=6,
n_redundant=2, random_state=42,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42,
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
return X_train, X_test, y_train, y_test
可运行演示(补齐 Mock 数据与 print 反馈):
from typing import Tuple
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
def make_data(n_samples: int = 600) -> Tuple[np.ndarray, np.ndarray,
np.ndarray, np.ndarray]:
"""生成标准分类数据集,标准化后返回。"""
X, y = make_classification(
n_samples=n_samples, n_features=10, n_informative=6,
n_redundant=2, random_state=42,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42,
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
return X_train, X_test, y_train, y_test
X_train, X_test, y_train, y_test = make_data(n_samples=200)
print("训练集:", X_train.shape, y_train.shape)
print("测试集:", X_test.shape, y_test.shape)
print("标准化后训练集均值约等于:", np.round(X_train.mean(axis=0)[:3], 3).tolist())
print("类别分布:", dict(zip(*np.unique(y_train, return_counts=True))))
Step 2:用 print_table 把 SVM 超参数讲清楚
痛点与机制:
print_table 让 SVM 的关键参数变成一张小抄。C 是犯错惩罚,kernel 是观察数据的方式,gamma 是 RBF 核里单个样本的影响半径。先看懂这些词,再看模型分数才不会迷路。
核心源码(逐字来自文末完整源码):
def print_table(headers: list[str], rows: list[list], col_widths: list[int] | None = None) -> None:
if col_widths is None:
col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
for i, h in enumerate(headers)]
sep = "┼".join("─" * (w + 2) for w in col_widths)
print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
print(f"├{sep}┤")
for row in rows:
print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")
可运行演示(补齐 Mock 数据与 print 反馈):
def print_table(headers: list[str], rows: list[list], col_widths: list[int] | None = None) -> None:
if col_widths is None:
col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
for i, h in enumerate(headers)]
sep = "┼".join("─" * (w + 2) for w in col_widths)
print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
print(f"├{sep}┤")
for row in rows:
print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")
print_table(
["参数", "小白理解", "调大后常见结果"],
[
["C", "犯错惩罚", "边界贴训练集更紧"],
["kernel", "看数据的方式", "linear/rbf/poly 适合不同形状"],
["gamma", "单个样本影响半径", "RBF 曲线更弯,易过拟合"],
],
)
Step 3:用 mode_linear 观察 C 如何影响最大间隔
痛点与机制:
线性 SVM 像在两类点之间修一条最宽的马路。C 小,模型更愿意容忍少量错分,马路更宽;C 大,模型更怕错分,边界会贴训练样本更紧,支持向量数量也会变化。
核心源码(逐字来自文末完整源码):
def mode_linear() -> None:
section("线性核 SVM(文本/高维特征场景)")
X_train, X_test, y_train, y_test = make_data()
rows: list[list] = []
for C in [0.01, 0.1, 1, 10, 100]:
clf = SVC(kernel="linear", C=C, random_state=42)
cv_scores = cross_val_score(clf, X_train, y_train, cv=5, scoring="accuracy")
clf.fit(X_train, y_train)
test_acc = accuracy_score(y_test, clf.predict(X_test))
n_sv = clf.n_support_.sum()
rows.append([f"C={C}", f"{cv_scores.mean():.4f}", f"±{cv_scores.std():.4f}",
f"{test_acc:.4f}", str(n_sv)])
print_table(
["C 值", "CV 均值", "CV 标准差", "测试准确率", "支持向量数"],
rows, [8, 8, 10, 10, 10],
)
print("\n 💡 C 越大,支持向量越少,间隔越小,训练集拟合越紧")
可运行演示(补齐 Mock 数据与 print 反馈):
import time
from typing import Tuple
import numpy as np
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def print_table(headers: list[str], rows: list[list], col_widths: list[int] | None = None) -> None:
if col_widths is None:
col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
for i, h in enumerate(headers)]
sep = "┼".join("─" * (w + 2) for w in col_widths)
print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
print(f"├{sep}┤")
for row in rows:
print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")
def make_data(n_samples: int = 600) -> Tuple[np.ndarray, np.ndarray,
np.ndarray, np.ndarray]:
"""生成标准分类数据集,标准化后返回。"""
X, y = make_classification(
n_samples=n_samples, n_features=10, n_informative=6,
n_redundant=2, random_state=42,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42,
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
return X_train, X_test, y_train, y_test
def mode_linear() -> None:
section("线性核 SVM(文本/高维特征场景)")
X_train, X_test, y_train, y_test = make_data()
rows: list[list] = []
for C in [0.01, 0.1, 1, 10, 100]:
clf = SVC(kernel="linear", C=C, random_state=42)
cv_scores = cross_val_score(clf, X_train, y_train, cv=5, scoring="accuracy")
clf.fit(X_train, y_train)
test_acc = accuracy_score(y_test, clf.predict(X_test))
n_sv = clf.n_support_.sum()
rows.append([f"C={C}", f"{cv_scores.mean():.4f}", f"±{cv_scores.std():.4f}",
f"{test_acc:.4f}", str(n_sv)])
print_table(
["C 值", "CV 均值", "CV 标准差", "测试准确率", "支持向量数"],
rows, [8, 8, 10, 10, 10],
)
print("\n 💡 C 越大,支持向量越少,间隔越小,训练集拟合越紧")
mode_linear()
Step 4:用 mode_kernels 对比 linear/poly/rbf 核函数
痛点与机制:
核函数像给数据换观察角度。线性核只能画直线,多项式核能表达特征交叉,RBF 核像给每个样本套一个高斯气泡,适合月牙形、同心圆这类非线性边界。
核心源码(逐字来自文末完整源码):
def mode_kernels() -> None:
section("三种核函数对比(非线性数据集)")
datasets = {
"make_moons(月牙形)": make_moons(n_samples=400, noise=0.15, random_state=42),
"make_circles(同心圆)": make_circles(n_samples=400, noise=0.1, factor=0.5, random_state=42),
"make_classification": make_classification(n_samples=400, n_features=2,
n_informative=2, n_redundant=0,
random_state=42),
}
kernels = [
("linear", {"C": 1.0}),
("poly", {"C": 1.0, "degree": 3, "gamma": "scale"}),
("rbf", {"C": 1.0, "gamma": "scale"}),
]
for ds_name, (X, y) in datasets.items():
print(f"\n 数据集: {ds_name}")
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.25, random_state=42)
rows = []
for kernel, params in kernels:
t0 = time.perf_counter()
clf = SVC(kernel=kernel, **params, random_state=42)
clf.fit(X_tr, y_tr)
elapsed = time.perf_counter() - t0
acc = accuracy_score(y_te, clf.predict(X_te))
rows.append([kernel, str(params), f"{acc:.4f}", f"{elapsed*1000:.1f}ms"])
print_table(["核函数", "参数", "测试准确率", "训练耗时"], rows, [8, 35, 10, 10])
可运行演示(补齐 Mock 数据与 print 反馈):
import time
import numpy as np
from sklearn.datasets import make_classification, make_circles, make_moons
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def print_table(headers: list[str], rows: list[list], col_widths: list[int] | None = None) -> None:
if col_widths is None:
col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
for i, h in enumerate(headers)]
sep = "┼".join("─" * (w + 2) for w in col_widths)
print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
print(f"├{sep}┤")
for row in rows:
print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")
def mode_kernels() -> None:
section("三种核函数对比(非线性数据集)")
datasets = {
"make_moons(月牙形)": make_moons(n_samples=400, noise=0.15, random_state=42),
"make_circles(同心圆)": make_circles(n_samples=400, noise=0.1, factor=0.5, random_state=42),
"make_classification": make_classification(n_samples=400, n_features=2,
n_informative=2, n_redundant=0,
random_state=42),
}
kernels = [
("linear", {"C": 1.0}),
("poly", {"C": 1.0, "degree": 3, "gamma": "scale"}),
("rbf", {"C": 1.0, "gamma": "scale"}),
]
for ds_name, (X, y) in datasets.items():
print(f"\n 数据集: {ds_name}")
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.25, random_state=42)
rows = []
for kernel, params in kernels:
t0 = time.perf_counter()
clf = SVC(kernel=kernel, **params, random_state=42)
clf.fit(X_tr, y_tr)
elapsed = time.perf_counter() - t0
acc = accuracy_score(y_te, clf.predict(X_te))
rows.append([kernel, str(params), f"{acc:.4f}", f"{elapsed*1000:.1f}ms"])
print_table(["核函数", "参数", "测试准确率", "训练耗时"], rows, [8, 35, 10, 10])
mode_kernels()
Step 5:用 mode_boundary 画出 ASCII 决策边界
痛点与机制:
决策边界图能把抽象模型变成地图:· 和 + 是模型预测区域,O 和 X 是真实样本。线性核边界更直,RBF 核边界更弯,这就是核技巧的直观效果。
核心源码(逐字来自文末完整源码):
def mode_boundary() -> None:
section("ASCII 决策边界可视化(2D 数据)")
# 月牙形数据,线性核 vs RBF 核
X, y = make_moons(n_samples=300, noise=0.2, random_state=42)
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.3, random_state=42)
for kernel in ("linear", "rbf"):
clf = SVC(kernel=kernel, C=1.0, gamma="scale", random_state=42)
clf.fit(X_tr, y_tr)
acc = accuracy_score(y_te, clf.predict(X_te))
# 构建 ASCII 网格
W, H = 50, 20
x_min, x_max = X_s[:, 0].min() - 0.3, X_s[:, 0].max() + 0.3
y_min, y_max = X_s[:, 1].min() - 0.3, X_s[:, 1].max() + 0.3
xs = np.linspace(x_min, x_max, W)
ys = np.linspace(y_max, y_min, H) # 纵轴翻转
xx, yy = np.meshgrid(xs, ys)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(H, W)
# 叠加真实样本点
grid = [list("·" if z == 0 else "+" for z in row) for row in Z]
for xi, yi, label in zip(X_s[:, 0], X_s[:, 1], y):
col = int((xi - x_min) / (x_max - x_min) * (W - 1))
row = int((y_max - yi) / (y_max - y_min) * (H - 1))
col = max(0, min(W - 1, col))
row = max(0, min(H - 1, row))
grid[row][col] = "O" if label == 0 else "X"
print(f"\n 核函数: {kernel} 测试准确率: {acc:.4f}")
print(f" {'─'*W}")
for row in grid:
print(f" {''.join(row)}")
print(f" {'─'*W}")
print(f" 图例: O=类0样本 X=类1样本 ·=预测类0区域 +=预测类1区域")
可运行演示(补齐 Mock 数据与 print 反馈):
import time
import numpy as np
from sklearn.datasets import make_moons
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def mode_boundary() -> None:
section("ASCII 决策边界可视化(2D 数据)")
# 月牙形数据,线性核 vs RBF 核
X, y = make_moons(n_samples=300, noise=0.2, random_state=42)
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.3, random_state=42)
for kernel in ("linear", "rbf"):
clf = SVC(kernel=kernel, C=1.0, gamma="scale", random_state=42)
clf.fit(X_tr, y_tr)
acc = accuracy_score(y_te, clf.predict(X_te))
# 构建 ASCII 网格
W, H = 50, 20
x_min, x_max = X_s[:, 0].min() - 0.3, X_s[:, 0].max() + 0.3
y_min, y_max = X_s[:, 1].min() - 0.3, X_s[:, 1].max() + 0.3
xs = np.linspace(x_min, x_max, W)
ys = np.linspace(y_max, y_min, H) # 纵轴翻转
xx, yy = np.meshgrid(xs, ys)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(H, W)
# 叠加真实样本点
grid = [list("·" if z == 0 else "+" for z in row) for row in Z]
for xi, yi, label in zip(X_s[:, 0], X_s[:, 1], y):
col = int((xi - x_min) / (x_max - x_min) * (W - 1))
row = int((y_max - yi) / (y_max - y_min) * (H - 1))
col = max(0, min(W - 1, col))
row = max(0, min(H - 1, row))
grid[row][col] = "O" if label == 0 else "X"
print(f"\n 核函数: {kernel} 测试准确率: {acc:.4f}")
print(f" {'─'*W}")
for row in grid:
print(f" {''.join(row)}")
print(f" {'─'*W}")
print(f" 图例: O=类0样本 X=类1样本 ·=预测类0区域 +=预测类1区域")
mode_boundary()
Step 6:用 main 做 linear/kernels/boundary/all 命令调度
痛点与机制:
main 是脚本遥控器。新手不用改源码,只要换 --mode,就能单独运行线性 SVM、核函数对比、决策边界,或用 all 一次跑完。
核心源码(逐字来自文末完整源码):
def main() -> None:
parser = argparse.ArgumentParser(description="SVM 支持向量机实战")
parser.add_argument(
"--mode",
choices=["linear", "kernels", "boundary", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"linear": mode_linear,
"kernels": mode_kernels,
"boundary": mode_boundary,
"all": lambda: [mode_linear(), mode_kernels(), mode_boundary()],
}
dispatch[args.mode]()
可运行演示(补齐 Mock 数据与 print 反馈):
import argparse
import sys
def mode_linear() -> None:
print("linear:观察 C 如何影响最大间隔和支持向量数量")
def mode_kernels() -> None:
print("kernels:对比 linear/poly/rbf 三种核函数")
def mode_boundary() -> None:
print("boundary:用 ASCII 图看线性核与 RBF 核的决策边界")
def main() -> None:
parser = argparse.ArgumentParser(description="SVM 支持向量机实战")
parser.add_argument(
"--mode",
choices=["linear", "kernels", "boundary", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"linear": mode_linear,
"kernels": mode_kernels,
"boundary": mode_boundary,
"all": lambda: [mode_linear(), mode_kernels(), mode_boundary()],
}
dispatch[args.mode]()
for mode in ["linear", "kernels", "boundary", "all"]:
print(f"\n$ python3 45-python-svm.py --mode {mode}")
sys.argv = ["prog", "--mode", mode]
main()
极客实战:完整源码与运行
现在,把上面的积木拼起来,将以下完整代码放进你的编辑器。建议先跑 --mode linear 观察 C,再跑 --mode kernels 对比核函数,最后跑 --mode boundary 看决策边界。
#!/usr/bin/env python3
"""
45-python-svm.py — SVM 支持向量机实战
用法:
python3 45-python-svm.py --mode linear # 线性核演示
python3 45-python-svm.py --mode kernels # 三种核函数对比
python3 45-python-svm.py --mode boundary # ASCII 决策边界可视化
python3 45-python-svm.py --mode all # 全部演示(默认)
零外部依赖(仅 sklearn/numpy),直接运行。
"""
import argparse
import time
from typing import Tuple
import numpy as np
from sklearn.datasets import make_classification, make_circles, make_moons
from sklearn.metrics import accuracy_score, classification_report
from sklearn.model_selection import cross_val_score, train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def section(title: str) -> None:
print(f"\n{'='*60}\n {title}\n{'='*60}")
def print_table(headers: list[str], rows: list[list], col_widths: list[int] | None = None) -> None:
if col_widths is None:
col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
for i, h in enumerate(headers)]
sep = "┼".join("─" * (w + 2) for w in col_widths)
print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
print(f"├{sep}┤")
for row in rows:
print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")
def make_data(n_samples: int = 600) -> Tuple[np.ndarray, np.ndarray,
np.ndarray, np.ndarray]:
"""生成标准分类数据集,标准化后返回。"""
X, y = make_classification(
n_samples=n_samples, n_features=10, n_informative=6,
n_redundant=2, random_state=42,
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42,
)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
return X_train, X_test, y_train, y_test
# ─── 模式1:线性核 ─────────────────────────────────────────────────────────────
def mode_linear() -> None:
section("线性核 SVM(文本/高维特征场景)")
X_train, X_test, y_train, y_test = make_data()
rows: list[list] = []
for C in [0.01, 0.1, 1, 10, 100]:
clf = SVC(kernel="linear", C=C, random_state=42)
cv_scores = cross_val_score(clf, X_train, y_train, cv=5, scoring="accuracy")
clf.fit(X_train, y_train)
test_acc = accuracy_score(y_test, clf.predict(X_test))
n_sv = clf.n_support_.sum()
rows.append([f"C={C}", f"{cv_scores.mean():.4f}", f"±{cv_scores.std():.4f}",
f"{test_acc:.4f}", str(n_sv)])
print_table(
["C 值", "CV 均值", "CV 标准差", "测试准确率", "支持向量数"],
rows, [8, 8, 10, 10, 10],
)
print("\n 💡 C 越大,支持向量越少,间隔越小,训练集拟合越紧")
# ─── 模式2:三种核函数对比 ─────────────────────────────────────────────────────
def mode_kernels() -> None:
section("三种核函数对比(非线性数据集)")
datasets = {
"make_moons(月牙形)": make_moons(n_samples=400, noise=0.15, random_state=42),
"make_circles(同心圆)": make_circles(n_samples=400, noise=0.1, factor=0.5, random_state=42),
"make_classification": make_classification(n_samples=400, n_features=2,
n_informative=2, n_redundant=0,
random_state=42),
}
kernels = [
("linear", {"C": 1.0}),
("poly", {"C": 1.0, "degree": 3, "gamma": "scale"}),
("rbf", {"C": 1.0, "gamma": "scale"}),
]
for ds_name, (X, y) in datasets.items():
print(f"\n 数据集: {ds_name}")
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.25, random_state=42)
rows = []
for kernel, params in kernels:
t0 = time.perf_counter()
clf = SVC(kernel=kernel, **params, random_state=42)
clf.fit(X_tr, y_tr)
elapsed = time.perf_counter() - t0
acc = accuracy_score(y_te, clf.predict(X_te))
rows.append([kernel, str(params), f"{acc:.4f}", f"{elapsed*1000:.1f}ms"])
print_table(["核函数", "参数", "测试准确率", "训练耗时"], rows, [8, 35, 10, 10])
# ─── 模式3:ASCII 决策边界可视化 ───────────────────────────────────────────────
def mode_boundary() -> None:
section("ASCII 决策边界可视化(2D 数据)")
# 月牙形数据,线性核 vs RBF 核
X, y = make_moons(n_samples=300, noise=0.2, random_state=42)
scaler = StandardScaler()
X_s = scaler.fit_transform(X)
X_tr, X_te, y_tr, y_te = train_test_split(X_s, y, test_size=0.3, random_state=42)
for kernel in ("linear", "rbf"):
clf = SVC(kernel=kernel, C=1.0, gamma="scale", random_state=42)
clf.fit(X_tr, y_tr)
acc = accuracy_score(y_te, clf.predict(X_te))
# 构建 ASCII 网格
W, H = 50, 20
x_min, x_max = X_s[:, 0].min() - 0.3, X_s[:, 0].max() + 0.3
y_min, y_max = X_s[:, 1].min() - 0.3, X_s[:, 1].max() + 0.3
xs = np.linspace(x_min, x_max, W)
ys = np.linspace(y_max, y_min, H) # 纵轴翻转
xx, yy = np.meshgrid(xs, ys)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(H, W)
# 叠加真实样本点
grid = [list("·" if z == 0 else "+" for z in row) for row in Z]
for xi, yi, label in zip(X_s[:, 0], X_s[:, 1], y):
col = int((xi - x_min) / (x_max - x_min) * (W - 1))
row = int((y_max - yi) / (y_max - y_min) * (H - 1))
col = max(0, min(W - 1, col))
row = max(0, min(H - 1, row))
grid[row][col] = "O" if label == 0 else "X"
print(f"\n 核函数: {kernel} 测试准确率: {acc:.4f}")
print(f" {'─'*W}")
for row in grid:
print(f" {''.join(row)}")
print(f" {'─'*W}")
print(f" 图例: O=类0样本 X=类1样本 ·=预测类0区域 +=预测类1区域")
# ─── 入口 ─────────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="SVM 支持向量机实战")
parser.add_argument(
"--mode",
choices=["linear", "kernels", "boundary", "all"],
default="all",
)
args = parser.parse_args()
dispatch = {
"linear": mode_linear,
"kernels": mode_kernels,
"boundary": mode_boundary,
"all": lambda: [mode_linear(), mode_kernels(), mode_boundary()],
}
dispatch[args.mode]()
if __name__ == "__main__":
main()
$ python3 45-python-svm.py --mode linear
============================================================
线性核 SVM(文本/高维特征场景)
============================================================
┌──────────┬──────────┬────────────┬────────────┬────────────┐
│ C 值 │ CV 均值 │ CV 标准差 │ 测试准确率 │ 支持向量数 │
├──────────┼──────────┼────────────┼────────────┼────────────┤
│ C=0.01 │ 0.8167 │ ±0.0214 │ 0.8833 │ 318 │
│ C=0.1 │ 0.8125 │ ±0.0342 │ 0.8833 │ 231 │
│ C=1 │ 0.8208 │ ±0.0369 │ 0.8917 │ 212 │
│ C=10 │ 0.8229 │ ±0.0378 │ 0.8917 │ 211 │
│ C=100 │ 0.8208 │ ±0.0375 │ 0.8833 │ 210 │
└──────────┴──────────┴────────────┴────────────┴────────────┘
💡 C 越大,支持向量越少,间隔越小,训练集拟合越紧
$ python3 45-python-svm.py --mode boundary
============================================================
ASCII 决策边界可视化(2D 数据)
============================================================
核函数: linear 测试准确率: 0.9000
──────────────────────────────────────────────────
··················································
·····················O····························
················OOOO····O·························
················OO··OOO··O························
··········O·O··OOOOOOO·O··OO······················
······O·O··OOOOOOO·OO·OOO·OO·····················+
·····O····O··O·O·XXO·OOOO··O················++++++
·········OOOO·O··O·X····XOOOOO·O·······X++++X+++++
·····O·OO·O····OO·XX··X·OOOOOOO···+XXX+X+XXX++++++
···O··OOOO···O·XX·XX···X·O·OO+O+O+X+XX++XX++++++++
·······OOOOO····X·XXXX··+O+OO+OO++++++X++++X++++++
··O···OO···O··X·····XX+XX++OOOXX++XX+XXX++++++X+++
···············++++X+XXXXO++++XXXXX+XXXX++XX++++++
··········O++++++++XXXXXX+XXXXXX++XXXX+XX+++X+++++
·····+++++++++++++++XXX+++++XXX+X+X+++X+++++++++++
+++++++++++++++++++++++XX++XX+XX+++XX+X+++++++++++
++++++++++++++++++++++X+++++++++XXX+X+++++++++++++
小结
| 模块 | 你要记住什么 |
|---|---|
make_data |
生成并标准化分类数据,避免尺度干扰 SVM 间隔 |
mode_linear |
用不同 C 值观察最大间隔和支持向量数量 |
mode_kernels |
对比 linear、poly、rbf 在非线性数据上的表现 |
mode_boundary |
用 ASCII 图看见直线边界和弯曲边界 |
main |
用 --mode 分层运行 SVM 实验 |
⏱ NexDo Time(5 分钟)
挑战:把 mode_boundary() 里的 kernel in ("linear", "rbf") 改成加入 "poly",重新运行 --mode boundary,观察多项式核的边界形状。
Don’t wait for next time, do it in the next moment.