文章

45 · SVM 支持向量机:最大间隔分类器

#024 · 2026-04-16 · Python

🔗 知识图谱导航:阅读本文前,建议先回顾《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 决策边界

痛点与机制

决策边界图能把抽象模型变成地图:·+ 是模型预测区域,OX 是真实样本。线性核边界更直,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.