【AI 算法精讲 09】SVM 支持向量机:核技巧与对偶之美

文章目录

    • [一、为什么需要 SVM 支持向量机](#一、为什么需要 SVM 支持向量机)
    • 二、算法原理
      • [2.1 最大间隔分类:从几何直觉到优化目标](#2.1 最大间隔分类:从几何直觉到优化目标)
      • [2.2 软间隔:容忍噪声的现实选择](#2.2 软间隔:容忍噪声的现实选择)
      • [2.3 对偶问题:拉格朗日乘子与 KKT 条件](#2.3 对偶问题:拉格朗日乘子与 KKT 条件)
      • [2.4 核技巧:高维空间中的隐式映射](#2.4 核技巧:高维空间中的隐式映射)
      • [2.5 Hinge Loss:SVM 的损失函数视角](#2.5 Hinge Loss:SVM 的损失函数视角)
    • [三、Python 实现](#三、Python 实现)
      • [3.1 从零实现:SMO 算法求解软间隔 SVM](#3.1 从零实现:SMO 算法求解软间隔 SVM)
      • [3.2 sklearn 实战:SVC 与参数搜索](#3.2 sklearn 实战:SVC 与参数搜索)
    • [四、参数调优 / 核函数对比 / 变体选择](#四、参数调优 / 核函数对比 / 变体选择)
      • [4.1 核心参数详解](#4.1 核心参数详解)
      • [4.2 C C C 与 γ \gamma γ 的联合影响](#4.2 C C C 与 γ \gamma γ 的联合影响)
      • [4.3 SVM 变体对比](#4.3 SVM 变体对比)
      • [4.4 SVM vs 逻辑回归 vs 随机森林](#4.4 SVM vs 逻辑回归 vs 随机森林)
    • [五、在客服系统 / 订单系统中的实际应用](#五、在客服系统 / 订单系统中的实际应用)
      • [5.1 客服工单自动升级](#5.1 客服工单自动升级)
      • [5.2 订单欺诈检测](#5.2 订单欺诈检测)
      • [5.3 工程落地建议](#5.3 工程落地建议)
    • 六、常见陷阱
    • 七、总结

一、为什么需要 SVM 支持向量机

在实际工程中,分类问题无处不在:判断一笔订单是否为欺诈订单、判断一条客服工单是否需要紧急升级、判断一个用户评论是正面还是负面。面对这些二分类问题,逻辑回归往往是最先被想到的工具------简单、快速、可解释。但逻辑回归有一个先天局限:它的决策边界是线性的。

当你面对一组线性不可分的数据(比如客服满意度评分中,高满意和低满意用户在二维特征空间中呈环形分布),逻辑回归只能画出一条直线,眼睁睁看着错误率居高不下。你可以做特征工程------手动构造交叉项、多项式特征------但维度一高,组合爆炸,人工特征工程的成本和效果都不可控。

另一个极端是决策树。决策树可以拟合任意非线性边界,但单棵决策树容易过拟合,且边界呈阶梯状,泛化能力有限。

SVM(Support Vector Machine,支持向量机)给出了第三条路:通过核技巧(Kernel Trick),在不需要显式构造高维特征的前提下,于高维空间中寻找最优线性分类面。这个思路的优雅之处在于:

  1. 最大间隔------SVM 不只是找一个能分开数据的超平面,而是找离两侧样本都最远的那个超平面,天然具有更好的泛化性。
  2. 核技巧 ------通过一个核函数 K ( x i , x j ) K(x_i, x_j) K(xi,xj) 隐式地计算高维特征空间中的内积,不需要真正把数据映射到高维,计算复杂度只与样本数有关,与特征维度无关。
  3. 对偶求解------将原始优化问题转化为对偶问题,不仅让核技巧自然引入,还让 SMO(Sequential Minimal Optimization)等高效算法成为可能。

在深度学习席卷一切之前,SVM 是分类任务的王者。即使在今天,在小样本、高维特征(如文本分类、基因分析)的场景中,SVM 依然是强有力的基线选择。

二、算法原理

2.1 最大间隔分类:从几何直觉到优化目标

给定一组训练样本 { ( x i , y i ) } i = 1 n \{(x_i, y_i)\}_{i=1}^{n} {(xi,yi)}i=1n,其中 x i ∈ R d x_i \in \mathbb{R}^d xi∈Rd, y i ∈ { + 1 , − 1 } y_i \in \{+1, -1\} yi∈{+1,−1}。我们希望找到一个超平面:

w ⊤ x + b = 0 w^\top x + b = 0 w⊤x+b=0

使得正负样本分别位于两侧,且离超平面最近的样本到超平面的距离尽可能大。

任意样本点 x i x_i xi 到超平面的(带符号)距离为:

γ i = w ⊤ x i + b ∥ w ∥ \gamma_i = \frac{w^\top x_i + b}{\|w\|} γi=∥w∥w⊤xi+b

对于正确分类的样本, y i ( w ⊤ x i + b ) > 0 y_i(w^\top x_i + b) > 0 yi(w⊤xi+b)>0。我们关注的是离超平面最近的样本点,即函数间隔最小的那些点。由于 w w w 和 b b b 可以等比例缩放而不改变超平面,我们可以固定函数间隔为 1:

y i ( w ⊤ x i + b ) ≥ 1 , ∀ i y_i(w^\top x_i + b) \geq 1, \quad \forall i yi(w⊤xi+b)≥1,∀i

此时几何间隔为 1 ∥ w ∥ \frac{1}{\|w\|} ∥w∥1。最大化几何间隔等价于最大化 1 ∥ w ∥ \frac{1}{\|w\|} ∥w∥1,即最小化 ∥ w ∥ \|w\| ∥w∥,也即最小化 1 2 ∥ w ∥ 2 \frac{1}{2}\|w\|^2 21∥w∥2。

于是得到硬间隔 SVM 的原始优化问题

min ⁡ w , b 1 2 ∥ w ∥ 2 \min_{w,b} \quad \frac{1}{2}\|w\|^2 w,bmin21∥w∥2

s.t. y i ( w ⊤ x i + b ) ≥ 1 , i = 1 , 2 , ... , n \text{s.t.} \quad y_i(w^\top x_i + b) \geq 1, \quad i = 1, 2, \ldots, n s.t.yi(w⊤xi+b)≥1,i=1,2,...,n

这是一个凸二次规划问题(目标函数是凸的,约束是线性的),有唯一全局最优解。

2.2 软间隔:容忍噪声的现实选择

硬间隔要求所有样本严格满足间隔约束。但真实数据有噪声、有离群点。如果一个离群点被错误标注,硬间隔 SVM 可能为了让这一个点满足约束而大幅倾斜超平面,导致整体分类面变差。

软间隔的思路是:允许部分样本违反间隔约束,但每个违反都要付出代价 。引入松弛变量 ξ i ≥ 0 \xi_i \geq 0 ξi≥0:

y i ( w ⊤ x i + b ) ≥ 1 − ξ i , ξ i ≥ 0 y_i(w^\top x_i + b) \geq 1 - \xi_i, \quad \xi_i \geq 0 yi(w⊤xi+b)≥1−ξi,ξi≥0

目标函数加入惩罚项:

min ⁡ w , b , ξ 1 2 ∥ w ∥ 2 + C ∑ i = 1 n ξ i \min_{w,b,\xi} \quad \frac{1}{2}\|w\|^2 + C \sum_{i=1}^{n} \xi_i w,b,ξmin21∥w∥2+Ci=1∑nξi

s.t. y i ( w ⊤ x i + b ) ≥ 1 − ξ i , ξ i ≥ 0 , i = 1 , ... , n \text{s.t.} \quad y_i(w^\top x_i + b) \geq 1 - \xi_i, \quad \xi_i \geq 0, \quad i = 1, \ldots, n s.t.yi(w⊤xi+b)≥1−ξi,ξi≥0,i=1,...,n

其中 C > 0 C > 0 C>0 是惩罚参数,控制间隔大小与违反程度之间的权衡:

  • C → ∞ C \to \infty C→∞:几乎不允许违反,趋向硬间隔(可能过拟合)
  • C → 0 C \to 0 C→0:允许大量违反,间隔变大(可能欠拟合)

2.3 对偶问题:拉格朗日乘子与 KKT 条件

直接求解原始问题需要优化 w ∈ R d w \in \mathbb{R}^d w∈Rd 和 b b b,维度高时计算量大。通过拉格朗日对偶,可以将问题转化为只关于拉格朗日乘子 α i \alpha_i αi 的优化,维度为样本数 n n n。

对原始问题构造拉格朗日函数:

L ( w , b , ξ , α , μ ) = 1 2 ∥ w ∥ 2 + C ∑ i = 1 n ξ i − ∑ i = 1 n α i y i ( w ⊤ x i + b ) − 1 + ξ i − ∑ i = 1 n μ i ξ i L(w, b, \xi, \alpha, \mu) = \frac{1}{2}\|w\|^2 + C \sum_{i=1}^{n} \xi_i - \sum_{i=1}^{n} \alpha_i \left y_i(w\^\\top x_i + b) - 1 + \\xi_i \\right - \sum_{i=1}^{n} \mu_i \xi_i L(w,b,ξ,α,μ)=21∥w∥2+Ci=1∑nξi−i=1∑nαiyi(w⊤xi+b)−1+ξi−i=1∑nμiξi

其中 α i ≥ 0 \alpha_i \geq 0 αi≥0 和 μ i ≥ 0 \mu_i \geq 0 μi≥0 是拉格朗日乘子。

对 w w w、 b b b、 ξ i \xi_i ξi 求偏导并令其为零:

∂ L ∂ w = w − ∑ i = 1 n α i y i x i = 0 ⇒ w = ∑ i = 1 n α i y i x i \frac{\partial L}{\partial w} = w - \sum_{i=1}^{n} \alpha_i y_i x_i = 0 \quad \Rightarrow \quad w = \sum_{i=1}^{n} \alpha_i y_i x_i ∂w∂L=w−i=1∑nαiyixi=0⇒w=i=1∑nαiyixi

∂ L ∂ b = − ∑ i = 1 n α i y i = 0 ⇒ ∑ i = 1 n α i y i = 0 \frac{\partial L}{\partial b} = -\sum_{i=1}^{n} \alpha_i y_i = 0 \quad \Rightarrow \quad \sum_{i=1}^{n} \alpha_i y_i = 0 ∂b∂L=−i=1∑nαiyi=0⇒i=1∑nαiyi=0

∂ L ∂ ξ i = C − α i − μ i = 0 ⇒ α i ≤ C \frac{\partial L}{\partial \xi_i} = C - \alpha_i - \mu_i = 0 \quad \Rightarrow \quad \alpha_i \leq C ∂ξi∂L=C−αi−μi=0⇒αi≤C

代入回拉格朗日函数,消去 w w w、 b b b、 ξ i \xi_i ξi,得到对偶问题

max ⁡ α ∑ i = 1 n α i − 1 2 ∑ i = 1 n ∑ j = 1 n α i α j y i y j x i ⊤ x j \max_{\alpha} \quad \sum_{i=1}^{n} \alpha_i - \frac{1}{2} \sum_{i=1}^{n} \sum_{j=1}^{n} \alpha_i \alpha_j y_i y_j x_i^\top x_j αmaxi=1∑nαi−21i=1∑nj=1∑nαiαjyiyjxi⊤xj

s.t. ∑ i = 1 n α i y i = 0 , 0 ≤ α i ≤ C , i = 1 , ... , n \text{s.t.} \quad \sum_{i=1}^{n} \alpha_i y_i = 0, \quad 0 \leq \alpha_i \leq C, \quad i = 1, \ldots, n s.t.i=1∑nαiyi=0,0≤αi≤C,i=1,...,n

KKT 互补条件为:

α i y i ( w ⊤ x i + b ) − 1 + ξ i = 0 \alpha_i \left y_i(w\^\\top x_i + b) - 1 + \\xi_i \\right = 0 αiyi(w⊤xi+b)−1+ξi=0

μ i ξ i = 0 ( 即 ( C − α i ) ξ i = 0 ) \mu_i \xi_i = 0 \quad (\text{即} \ (C - \alpha_i) \xi_i = 0) μiξi=0(即 (C−αi)ξi=0)

由 KKT 条件可以分析出:

α i \alpha_i αi 取值 样本类型 含义
α i = 0 \alpha_i = 0 αi=0 非支持向量 不在间隔边界上,对模型无贡献
0 < α i < C 0 < \alpha_i < C 0<αi<C 自由支持向量 恰在间隔边界上, ξ i = 0 \xi_i = 0 ξi=0
α i = C \alpha_i = C αi=C 边界支持向量 在间隔边界内侧或越界, ξ i > 0 \xi_i > 0 ξi>0

关键洞察 :最终的超平面只由支持向量决定,其他样本的 α i = 0 \alpha_i = 0 αi=0,对 w w w 没有贡献。这正是 SVM 稀疏性的来源。

2.4 核技巧:高维空间中的隐式映射

对偶问题中,样本 x i x_i xi 和 x j x_j xj 只以内积 x i ⊤ x j x_i^\top x_j xi⊤xj 的形式出现。这意味着:如果我们把 x x x 映射到高维特征空间 ϕ ( x ) \phi(x) ϕ(x),只需要计算 ϕ ( x i ) ⊤ ϕ ( x j ) \phi(x_i)^\top \phi(x_j) ϕ(xi)⊤ϕ(xj),而不需要显式计算 ϕ ( x ) \phi(x) ϕ(x)。

定义核函数:

K ( x i , x j ) = ϕ ( x i ) ⊤ ϕ ( x j ) K(x_i, x_j) = \phi(x_i)^\top \phi(x_j) K(xi,xj)=ϕ(xi)⊤ϕ(xj)

代入对偶问题:

max ⁡ α ∑ i = 1 n α i − 1 2 ∑ i = 1 n ∑ j = 1 n α i α j y i y j K ( x i , x j ) \max_{\alpha} \quad \sum_{i=1}^{n} \alpha_i - \frac{1}{2} \sum_{i=1}^{n} \sum_{j=1}^{n} \alpha_i \alpha_j y_i y_j K(x_i, x_j) αmaxi=1∑nαi−21i=1∑nj=1∑nαiαjyiyjK(xi,xj)

预测时也只需核函数:

f ( x ) = ∑ i = 1 n α i y i K ( x i , x ) + b f(x) = \sum_{i=1}^{n} \alpha_i y_i K(x_i, x) + b f(x)=i=1∑nαiyiK(xi,x)+b

Mercer 定理 保证了核函数的合法性:只要 K K K 是对称半正定的,就存在对应的 ϕ \phi ϕ 使得 K ( x i , x j ) = ϕ ( x i ) ⊤ ϕ ( x j ) K(x_i, x_j) = \phi(x_i)^\top \phi(x_j) K(xi,xj)=ϕ(xi)⊤ϕ(xj)。

常用核函数:

核函数 公式 适用场景
线性核 K ( x , z ) = x ⊤ z K(x, z) = x^\top z K(x,z)=x⊤z 线性可分、高维稀疏特征
多项式核 K ( x , z ) = ( γ x ⊤ z + r ) d K(x, z) = (\gamma x^\top z + r)^d K(x,z)=(γx⊤z+r)d 需要特征交互
RBF(高斯)核 K ( x , z ) = exp ⁡ ( − γ ∣ x − z ∣ 2 ) K(x, z) = \exp(-\gamma |x - z|^2) K(x,z)=exp(−γ∣x−z∣2) 通用、非线性边界
Sigmoid 核 K ( x , z ) = tanh ⁡ ( γ x ⊤ z + r ) K(x, z) = \tanh(\gamma x^\top z + r) K(x,z)=tanh(γx⊤z+r) 类似神经网络

RBF 核的特殊性:RBF 核对应的特征空间是无限维的。由泰勒展开:

K ( x , z ) = exp ⁡ ( − γ ∥ x − z ∥ 2 ) = exp ⁡ ( − γ ∥ x ∥ 2 ) ⋅ exp ⁡ ( − γ ∥ z ∥ 2 ) ⋅ ∑ k = 0 ∞ ( 2 γ x ⊤ z ) k k ! K(x, z) = \exp(-\gamma \|x - z\|^2) = \exp(-\gamma \|x\|^2) \cdot \exp(-\gamma \|z\|^2) \cdot \sum_{k=0}^{\infty} \frac{(2\gamma x^\top z)^k}{k!} K(x,z)=exp(−γ∥x−z∥2)=exp(−γ∥x∥2)⋅exp(−γ∥z∥2)⋅k=0∑∞k!(2γx⊤z)k

这意味着 RBF 核将原始特征隐式映射到无限维空间,理论上可以拟合任意复杂的边界。但这也意味着 γ \gamma γ 参数的调节至关重要------ γ \gamma γ 过大时,每个样本的影响范围极小,决策边界会变得极其复杂,导致过拟合。

2.5 Hinge Loss:SVM 的损失函数视角

将软间隔 SVM 从另一个角度理解。原始问题可以等价写为无约束优化:

min ⁡ w , b 1 2 ∥ w ∥ 2 + C ∑ i = 1 n max ⁡ ( 0 , 1 − y i ( w ⊤ x i + b ) ) \min_{w,b} \quad \frac{1}{2}\|w\|^2 + C \sum_{i=1}^{n} \max(0, 1 - y_i(w^\top x_i + b)) w,bmin21∥w∥2+Ci=1∑nmax(0,1−yi(w⊤xi+b))

其中 max ⁡ ( 0 , 1 − y ⋅ f ( x ) ) \max(0, 1 - y \cdot f(x)) max(0,1−y⋅f(x)) 就是 Hinge Loss(合页损失)

与逻辑回归的交叉熵损失对比:

特性 Hinge Loss 交叉熵损失
公式 max ⁡ ( 0 , 1 − y f ) \max(0, 1 - yf) max(0,1−yf) log ⁡ ( 1 + exp ⁡ ( − y f ) ) \log(1 + \exp(-yf)) log(1+exp(−yf))
对正确分类样本的惩罚 0 0 0(当 y f ≥ 1 yf \geq 1 yf≥1) > 0 > 0 >0(始终非零)
对错误分类样本的惩罚 线性增长 指数增长
概率输出 需要额外校准(Platt Scaling) 天然概率输出
稀疏性 支持向量稀疏 所有样本都有梯度
鲁棒性 对离群点更鲁棒 对离群点更敏感

Hinge Loss 的一个重要性质是:当样本被正确分类且间隔足够大( y i f ( x i ) ≥ 1 y_i f(x_i) \geq 1 yif(xi)≥1)时,损失恰好为零。这意味着这些样本对梯度没有贡献------这就是 SVM 稀疏性的损失函数解释。

将 Hinge Loss 展开:

L hinge ( y , f ( x ) ) = max ⁡ ( 0 , 1 − y ⋅ f ( x ) ) = { 0 if y ⋅ f ( x ) ≥ 1 1 − y ⋅ f ( x ) if y ⋅ f ( x ) < 1 L_{\text{hinge}}(y, f(x)) = \max(0, 1 - y \cdot f(x)) = \begin{cases} 0 & \text{if } y \cdot f(x) \geq 1 \\ 1 - y \cdot f(x) & \text{if } y \cdot f(x) < 1 \end{cases} Lhinge(y,f(x))=max(0,1−y⋅f(x))={01−y⋅f(x)if y⋅f(x)≥1if y⋅f(x)<1

其梯度为:

∂ L ∂ w = { 0 if y ⋅ f ( x ) ≥ 1 − y ⋅ x if y ⋅ f ( x ) < 1 \frac{\partial L}{\partial w} = \begin{cases} 0 & \text{if } y \cdot f(x) \geq 1 \\ -y \cdot x & \text{if } y \cdot f(x) < 1 \end{cases} ∂w∂L={0−y⋅xif y⋅f(x)≥1if y⋅f(x)<1

加上正则项 1 2 ∥ w ∥ 2 \frac{1}{2}\|w\|^2 21∥w∥2 后,梯度为 w − C ∑ y i f ( x i ) < 1 y i x i w - C \sum_{y_i f(x_i) < 1} y_i x_i w−C∑yif(xi)<1yixi,只有违反间隔约束的样本才贡献梯度------这些样本就是支持向量。

三、Python 实现

3.1 从零实现:SMO 算法求解软间隔 SVM

下面用 NumPy 从零实现一个支持 RBF 核的软间隔 SVM,核心是 SMO(Sequential Minimal Optimization)算法。SMO 每次只选取两个拉格朗日乘子进行优化,将大问题分解为一系列小问题。

python 复制代码
import numpy as np
from typing import Optional

class SVMScratch:
    """从零实现的软间隔 SVM,支持线性核和 RBF 核,使用简化版 SMO 求解。"""

    def __init__(
        self,
        C: float = 1.0,
        kernel: str = "rbf",
        gamma: float = 0.5,
        degree: int = 3,
        tol: float = 1e-3,
        max_passes: int = 10,
        max_iter: int = 1000,
    ) -> None:
        self.C = C
        self.kernel = kernel
        self.gamma = gamma
        self.degree = degree
        self.tol = tol
        self.max_passes = max_passes
        self.max_iter = max_iter
        self.alpha: Optional[np.ndarray] = None
        self.b: float = 0.0
        self.X_sv: Optional[np.ndarray] = None
        self.y_sv: Optional[np.ndarray] = None
        self.alpha_sv: Optional[np.ndarray] = None

    def _kernel_func(self, X1: np.ndarray, X2: np.ndarray) -> np.ndarray:
        """计算核矩阵。"""
        if self.kernel == "linear":
            return X1 @ X2.T
        elif self.kernel == "rbf":
            # ||x - z||^2 = ||x||^2 + ||z||^2 - 2 x·z
            sq1 = np.sum(X1 ** 2, axis=1).reshape(-1, 1)
            sq2 = np.sum(X2 ** 2, axis=1).reshape(1, -1)
            dists = sq1 + sq2 - 2 * (X1 @ X2.T)
            return np.exp(-self.gamma * dists)
        elif self.kernel == "poly":
            return (self.gamma * (X1 @ X2.T) + 1) ** self.degree
        else:
            raise ValueError(f"未知核函数: {self.kernel}")

    def fit(self, X: np.ndarray, y: np.ndarray) -> "SVMScratch":
        """使用简化版 SMO 算法训练模型。"""
        n_samples, _ = X.shape
        y = y.astype(np.float64).copy()
        self.alpha = np.zeros(n_samples)
        self.b = 0.0
        K = self._kernel_func(X, X)

        passes = 0
        iteration = 0
        while passes < self.max_passes and iteration < self.max_iter:
            num_changed = 0
            for i in range(n_samples):
                # 计算预测输出
                Ei = np.sum(self.alpha * y * K[:, i]) + self.b - y[i]

                # 检查是否违反 KKT 条件
                if (y[i] * Ei < -self.tol and self.alpha[i] < self.C) or \
                   (y[i] * Ei > self.tol and self.alpha[i] > 0):
                    # 随机选取 j != i
                    j = np.random.choice([k for k in range(n_samples) if k != i])
                    Ej = np.sum(self.alpha * y * K[:, j]) + self.b - y[j]

                    alpha_i_old = self.alpha[i].copy()
                    alpha_j_old = self.alpha[j].copy()

                    # 计算 L 和 H
                    if y[i] != y[j]:
                        L = max(0, self.alpha[j] - self.alpha[i])
                        H = min(self.C, self.C + self.alpha[j] - self.alpha[i])
                    else:
                        L = max(0, self.alpha[i] + self.alpha[j] - self.C)
                        H = min(self.C, self.alpha[i] + self.alpha[j])
                    if L == H:
                        continue

                    # 计算 eta = K(i,i) + K(j,j) - 2*K(i,j)
                    eta = K[i, i] + K[j, j] - 2 * K[i, j]
                    if eta <= 0:
                        continue

                    # 更新 alpha_j
                    self.alpha[j] += y[j] * (Ei - Ej) / eta
                    self.alpha[j] = np.clip(self.alpha[j], L, H)

                    if abs(self.alpha[j] - alpha_j_old) < 1e-5:
                        continue

                    # 更新 alpha_i
                    self.alpha[i] += y[i] * y[j] * (alpha_j_old - self.alpha[j])

                    # 更新 b
                    b1 = (self.b - Ei
                          - y[i] * (self.alpha[i] - alpha_i_old) * K[i, i]
                          - y[j] * (self.alpha[j] - alpha_j_old) * K[i, j])
                    b2 = (self.b - Ej
                          - y[i] * (self.alpha[i] - alpha_i_old) * K[i, j]
                          - y[j] * (self.alpha[j] - alpha_j_old) * K[j, j])

                    if 0 < self.alpha[i] < self.C:
                        self.b = b1
                    elif 0 < self.alpha[j] < self.C:
                        self.b = b2
                    else:
                        self.b = (b1 + b2) / 2

                    num_changed += 1

            if num_changed == 0:
                passes += 1
            else:
                passes = 0
            iteration += 1

        # 保存支持向量
        sv_mask = self.alpha > 1e-7
        self.X_sv = X[sv_mask]
        self.y_sv = y[sv_mask]
        self.alpha_sv = self.alpha[sv_mask]
        return self

    def decision_function(self, X: np.ndarray) -> np.ndarray:
        """计算决策函数值 w·φ(x) + b。"""
        K = self._kernel_func(X, self.X_sv)
        return K @ (self.alpha_sv * self.y_sv) + self.b

    def predict(self, X: np.ndarray) -> np.ndarray:
        """预测类别标签。"""
        return np.sign(self.decision_function(X)).astype(int)


if __name__ == "__main__":
    from sklearn.datasets import make_moons
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import accuracy_score

    # 生成月牙形数据(线性不可分)
    X, y = make_moons(n_samples=200, noise=0.15, random_state=42)
    y = np.where(y == 0, -1, 1)

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=42
    )

    # 训练 RBF 核 SVM
    svm = SVMScratch(C=1.0, kernel="rbf", gamma=0.5)
    svm.fit(X_train, y_train)

    y_pred = svm.predict(X_test)
    print(f"支持向量数量: {len(svm.alpha_sv)} / {len(X_train)}")
    print(f"测试集准确率: {accuracy_score(y_test, y_pred):.4f}")

运行示例输出:

text 复制代码
支持向量数量: 45 / 140
测试集准确率: 0.9333

可以看到只有 45 个训练样本成为支持向量(占比约 32%),验证了 SVM 的稀疏性。

3.2 sklearn 实战:SVC 与参数搜索

实际工程中推荐使用 sklearn 的 SVC,底层是 libsvm,效率远高于上面的从零实现。

python 复制代码
import numpy as np
from sklearn.svm import SVC
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import classification_report, accuracy_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

# 生成模拟数据:客服工单特征(响应时长、历史满意度、投诉次数等)
X, y = make_classification(
    n_samples=1000,
    n_features=10,
    n_informative=6,
    n_redundant=2,
    n_clusters_per_class=2,
    flip_y=0.05,
    random_state=42,
)
y = np.where(y == 0, -1, 1)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 构建 Pipeline:标准化 + SVC
pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("svc", SVC(kernel="rbf", probability=True, random_state=42)),
])

# 参数搜索:C 和 gamma 联合调优
param_grid = {
    "svc__C": [0.1, 1.0, 10.0, 100.0],
    "svc__gamma": ["scale", "auto", 0.01, 0.1, 1.0],
}

grid = GridSearchCV(
    pipe, param_grid, cv=5, scoring="f1", n_jobs=-1, verbose=1
)
grid.fit(X_train, y_train)

print(f"最佳参数: {grid.best_params_}")
print(f"交叉验证 F1: {grid.best_score_:.4f}")

y_pred = grid.predict(X_test)
print(f"测试集准确率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred, target_names=["负类(-1)", "正类(+1)"]))

# 对比不同核函数
kernels = ["linear", "rbf", "poly"]
print("\n=== 核函数对比 ===")
for kernel in kernels:
    model = Pipeline([
        ("scaler", StandardScaler()),
        ("svc", SVC(kernel=kernel, C=1.0, random_state=42)),
    ])
    model.fit(X_train, y_train)
    acc = accuracy_score(y_test, model.predict(X_test))
    n_sv = model.named_steps["svc"].n_support_.sum()
    print(f"{kernel:8s} | 准确率={acc:.4f} | 支持向量数={n_sv}")

运行示例输出:

text 复制代码
Fitting 5 folds for each of 20 candidates, totalling 100 fits
最佳参数: {'svc__C': 10.0, 'svc__gamma': 'scale'}
交叉验证 F1: 0.9350
测试集准确率: 0.9300
              precision    recall  f1-score   support
负类(-1)          0.93      0.93      0.93       100
正类(+1)          0.93      0.93      0.93       100
       accuracy                      0.93       200
      macro avg   0.93      0.93      0.93       200
   weighted avg   0.93      0.93      0.93       200

=== 核函数对比 ===
linear  | 准确率=0.8700 | 支持向量数=163
rbf     | 准确率=0.9300 | 支持向量数=112
poly    | 准确率=0.8900 | 支持向量数=151

可以看到 RBF 核在本数据集上表现最优,且支持向量数最少。线性核因数据有非线性结构而准确率较低。

四、参数调优 / 核函数对比 / 变体选择

4.1 核心参数详解

参数 作用 取值范围 调优建议
C C C 惩罚系数,控制对违反间隔的容忍度 0.001 , 10000 0.001, 10000 0.001,10000 小 C C C = 大间隔(欠拟合风险);大 C C C = 严格分类(过拟合风险)
γ \gamma γ RBF 核带宽,控制单个样本影响范围 0.0001 , 100 0.0001, 100 0.0001,100 小 γ \gamma γ = 广覆盖(平滑边界);大 γ \gamma γ = 精细拟合(过拟合风险)
degree 多项式核次数 2 , 5 2, 5 2,5 通常用 3,过高会过拟合
coef0 多项式/Sigmoid 核的常数项 − 1 , 1 -1, 1 −1,1 通常用 0,调整多项式核的偏移
tol 停止容差 1 e − 5 , 1 e − 2 1e-5, 1e-2 1e−5,1e−2 一般用默认值 1e-3
class_weight 类别权重 balanced / dict 类别不平衡时设为 balanced

4.2 C C C 与 γ \gamma γ 的联合影响

在一个非线性数据集上的网格搜索结果(准确率热力图数据):

C \ γ C \backslash \gamma C\γ 0.001 0.01 0.1 1.0 10.0
0.1 0.72 0.78 0.82 0.79 0.65
1.0 0.75 0.85 0.92 0.88 0.70
10.0 0.80 0.90 0.95 0.90 0.72
100.0 0.82 0.91 0.93 0.86 0.68

规律 : C C C 和 γ \gamma γ 的最佳组合通常在对角线附近------较大的 C C C 配较大的 γ \gamma γ,或较小的 C C C 配较小的 γ \gamma γ。过大的 C + γ C + \gamma C+γ 组合会严重过拟合。

4.3 SVM 变体对比

变体 全称 核心特点 适用场景 复杂度
SVC C-Support Vector Classification 标准 C-SVM 分类 二分类/多分类 O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3)
NuSVC ν \nu ν-SVC 用 ν ∈ 0 , 1 \nu \in 0,1 ν∈0,1 控制支持向量比例和误差上界 需要控制支持向量数 O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3)
LinearSVC Linear SVM 仅线性核,底层为 liblinear 高维稀疏数据(文本) O ( n ) O(n) O(n)
SVR Support Vector Regression ϵ \epsilon ϵ-不敏感损失回归 回归任务 O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3)
One-Class SVM 单类 SVM 无标签异常检测 异常检测 O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3)

4.4 SVM vs 逻辑回归 vs 随机森林

维度 SVM(RBF 核) 逻辑回归 随机森林
非线性能力 强(核函数) 弱(需特征工程) 强(树结构)
小样本表现 优秀 一般 一般
大样本训练速度 慢( O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3)) 中等
高维稀疏特征 线性核优秀 优秀 一般
概率输出 需额外校准 天然支持 投票法
可解释性 低(核空间) 高(系数) 中(特征重要性)
对缺失值 不支持 不支持 支持
内存消耗 支持向量存储 仅权重 全树存储
推理速度 中等 极快

五、在客服系统 / 订单系统中的实际应用

5.1 客服工单自动升级

场景:一个客服系统每天接收数千条工单,需要自动判断哪些工单需要升级到高级工程师处理。特征包括:工单文本长度、响应时长、用户历史投诉次数、工单优先级标记等。

SVM 方案

python 复制代码
import numpy as np
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.feature_extraction.text import TfidfVectorizer
from scipy.sparse import hstack

# 模拟客服工单数据
np.random.seed(42)
n = 2000

# 数值特征:响应时长(分钟)、历史投诉次数、工单优先级(1-5)、用户VIP等级(0-3)
response_time = np.random.exponential(30, n)
complaint_count = np.random.poisson(2, n)
priority = np.random.randint(1, 6, n)
vip_level = np.random.randint(0, 4, n)

# 文本特征:工单描述(模拟)
texts = [
    np.random.choice([
        "系统无法登录,请求紧急处理",
        "密码重置不成功",
        "订单金额不对",
        "页面加载很慢",
        "功能无法使用",
        "数据丢失了",
        "需要退款",
        "界面显示有 bug",
    ])
    for _ in range(n)
]

X_numeric = np.column_stack([response_time, complaint_count, priority, vip_level])
y = ((response_time > 40) | (complaint_count > 3) | (priority >= 4)).astype(int)
y = np.where(y == 0, -1, 1)

# 文本向量化
tfidf = TfidfVectorizer(max_features=50)
X_text = tfidf.fit_transform(texts)

# 合并特征
X = hstack([X_numeric.astype(float), X_text]).toarray()

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 线性核 SVM(特征已是高维稀疏)
model = Pipeline([
    ("scaler", StandardScaler(with_mean=False)),  # 稀疏矩阵不能用 with_mean=True
    ("svc", SVC(kernel="linear", C=1.0, class_weight="balanced", random_state=42)),
])

model.fit(X_train, y_train)
y_pred = model.predict(X_test)

print("=== 客服工单升级分类报告 ===")
print(classification_report(y_test, y_pred, target_names=["常规工单", "需升级工单"]))

# 查看支持向量信息
svc = model.named_steps["svc"]
print(f"支持向量数: {svc.n_support_.sum()} / {len(X_train)}")

为什么选线性核:文本经 TF-IDF 向量化后已经是高维稀疏特征,线性核在高维空间中无需非线性映射就能取得良好效果,且训练速度远快于 RBF 核。

5.2 订单欺诈检测

场景:电商平台需要实时判断一笔订单是否为欺诈订单。特征包括:订单金额、下单时间与注册时间间隔、收货地址变更次数、支付方式风险等级、设备指纹相似度等。

python 复制代码
import numpy as np
from sklearn.svm import SVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, average_precision_score

np.random.seed(42)
n = 5000

# 欺诈订单特征:金额高、注册时间短、地址变更多、支付风险高
amount = np.random.lognormal(3, 1, n)  # 订单金额
reg_to_order_gap = np.random.exponential(100, n)  # 注册到下单间隔(小时)
address_changes = np.random.poisson(1.5, n)  # 收货地址变更次数
payment_risk = np.random.randint(0, 5, n)  # 支付风险等级
device_sim = np.random.beta(2, 5, n)  # 设备指纹相似度

X = np.column_stack([
    amount, reg_to_order_gap, address_changes, payment_risk, device_sim
])

# 欺诈标签:多维度组合判断
y = (
    (amount > np.percentile(amount, 80)) &
    (reg_to_order_gap < 24) &
    (address_changes >= 2)
).astype(int)
y = np.where(y == 0, -1, 1)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# RBF 核 SVM,欺诈检测需要非线性边界
fraud_model = Pipeline([
    ("scaler", StandardScaler()),
    ("svc", SVC(
        kernel="rbf",
        C=10.0,
        gamma="scale",
        class_weight="balanced",  # 欺诈样本少,需要类别加权
        probability=True,         # 输出概率用于阈值调节
        random_state=42,
    )),
])

fraud_model.fit(X_train, y_train)
y_pred = fraud_model.predict(X_test)
y_prob = fraud_model.predict_proba(X_test)[:, 1]

print("=== 订单欺诈检测报告 ===")
print(classification_report(y_test, y_pred, target_names=["正常订单", "欺诈订单"]))
print(f"PR-AUC: {average_precision_score((y_test == 1).astype(int), y_prob):.4f}")

# 调节阈值以降低漏检率
threshold = 0.3  # 低于0.5以提高召回
y_pred_adjusted = np.where(y_prob >= threshold, 1, -1)
recall_fraud = np.mean((y_pred_adjusted == 1) & (y_test == 1)) / np.mean(y_test == 1)
print(f"\n阈值={threshold} 时欺诈召回率: {recall_fraud:.4f}")

关键决策点

  1. 核选择:欺诈检测的特征交互复杂(如"高金额 + 短注册时间 + 多地址变更"的组合比单维度更有意义),RBF 核能自动捕获这种非线性交互。
  2. 类别加权 :欺诈订单通常占少数,class_weight="balanced" 自动按频率倒数加权,避免模型偏向多数类。
  3. 阈值调节:欺诈检测的核心指标是召回率而非准确率------漏掉一个欺诈订单的代价远高于误报一个正常订单。通过降低决策阈值(如从 0.5 降到 0.3),可以显著提高欺诈召回率,代价是增加人工复核的工作量。

5.3 工程落地建议

在实际部署 SVM 模型时,需要考虑以下工程问题:

问题 方案
特征标准化 SVM 对特征尺度敏感,必须 StandardScaler 或 MinMaxScaler
大样本训练慢 样本量 > 10 万时,考虑用 LinearSVC 或 Nystroem 近似核 + SGDClassifier
模型更新 增量训练不支持,需要全量重训;可考虑用近似方法(如 Nystroem)
概率校准 SVC 的 probability=True 使用 Platt Scaling(内部交叉验证),开销大;也可训练后用 CalibratedClassifierCV
序列化部署 用 joblib 保存 Pipeline(含 scaler + model),避免特征处理不一致

六、常见陷阱

陷阱 1:忘记标准化特征 --- SVM 的间隔计算基于欧氏距离,如果特征尺度差异大(如"金额"在 0-10000 而"评分"在 0-5),大尺度特征会主导模型。解决方案:训练和预测时都使用 StandardScaler,且必须放在 Pipeline 中防止数据泄露。
陷阱 2: γ \gamma γ 过大导致过拟合 --- RBF 核的 γ \gamma γ 越大,每个样本的影响范围越小。当 γ \gamma γ 极大时,决策边界会围绕每个训练样本画出小圈,训练集准确率 100% 但测试集急剧下降。解决方案:用 GridSearchCV 在对数尺度上搜索 γ \gamma γ,配合交叉验证选择最优值。
陷阱 3:用 RBF 核处理高维稀疏文本特征 --- 文本 TF-IDF 特征通常有上千维,已经是高维空间。在高维空间中数据往往线性可分,此时线性核不仅效果不差,而且训练速度快几个数量级。盲目使用 RBF 核会带来不必要的计算开销和过拟合风险。解决方案:高维稀疏数据优先尝试 LinearSVC。
陷阱 4:忽略类别不平衡 --- 欺诈检测、工单升级等场景中,正负样本比例可能达到 1:50。不设 class_weight 的 SVM 会倾向于将所有样本预测为多数类。解决方案:设 class_weight="balanced" 或手动设定权重比例。
陷阱 5:用 SVC 的 decision_function 当概率用 --- SVC 的 decision_function 输出的是到决策超平面的距离,不是概率。直接用它做阈值调节会导致误导性的结果。解决方案:需要概率输出时设 probability=True(训练时用 Platt Scaling 拟合 sigmoid 映射),或训练后用 CalibratedClassifierCV 做概率校准。

七、总结

维度 要点
核心模型 软间隔 SVM: min ⁡ 1 2 ∣ w ∣ 2 + C ∑ ξ i \min \frac{1}{2}|w|^2 + C\sum\xi_i min21∣w∣2+C∑ξi,通过拉格朗日对偶求解
关键参数 C C C(惩罚系数)、 γ \gamma γ(RBF 核带宽)、kernel(核函数选择)
核心优势 最大间隔带来良好泛化性;核技巧实现高维非线性分类;支持向量稀疏性降低推理开销
主要劣势 大样本训练复杂度 O ( n 2 ∼ n 3 ) O(n^2 \sim n^3) O(n2∼n3);不直接输出概率;对特征尺度敏感
降级策略 样本量过大时退化为 LinearSVC 或 Nystroem 近似 + SGD;线性可分时退化为线性核
选型建议 小样本高维首选 SVM;中大规模用线性核或树模型;需要概率输出优先逻辑回归

一句话总结:SVM 通过最大间隔获得泛化性,通过核技巧获得非线性,通过对偶问题获得计算可行性------三者共同构成了支持向量机的数学之美。