SVM支持向量机

如果给定的一堆向量无法区分,那就给他们通过某种程度升维,人为再造出一个维度。可以理解为创造的这个线性相关(但其实不是线性相关)的矩阵,就恰好能区分数据集。

人为给数据升维的方式成为核函数

数学推导

为了方便,我们假定二分类数据,是以0这个超平面对称分布。在二分类数据集画两条线,尽量让线中间的距离增大。这样尽可能区分1和-1数据。

  1. 线 A (恶意侧)\(w^T x + b = 1\)
  2. 线 B (正常侧)\(w^T x + b = -1\)

1. 找两个代表点

首先,我们在两条线上各找一个“代表”:

  • 在线 A 上找一个点 \(x_{pos}\),它满足 \(w^T x_{pos} + b = 1\)
  • 在线 B 上找一个点 \(x_{neg}\),它满足 \(w^T x_{neg} + b = -1\)

2. 算向量投影

这两条线AB的距离,我们可以认为是原点到线A和线B的和,根据点到直线的距离公式,距离为

\[d = \frac{2}{\|w\|} \]

我们的目标是

\[\max \quad \frac{2}{\|\boldsymbol{w}\|} \]

在数学上,最大化一个正的分式 \(\frac{2}{\|\boldsymbol{w}\|}\),完全等价于最小化它的倒数 \(\frac{\|\boldsymbol{w}\|}{2}\)

\[\max \frac{2}{\|\boldsymbol{w}\|} \iff \min \frac{\|\boldsymbol{w}\|}{2} \]

因为模长带有根号,在一会的拉格朗日数乘法计算过程中会十分复杂。
为了让数学过程变得“优雅”且易于计算:

  1. 平方处理:最小化 \(\|\boldsymbol{w}\|\) 等价于最小化 \(\|\boldsymbol{w}\|^2\)。平方之后,根号消失了,变成了一个平滑的二次函数。
  2. 乘以 1/2:在前面加一个 \(\frac{1}{2}\) 是为了在求导时,把平方项落下来的那个 \(2\) 给正好抵消掉(即 \(( \frac{1}{2} w^2 )' = w\))。

于是,最终的目标函数为:

\[\min_{\boldsymbol{w}, b} \quad \frac{1}{2}\|\boldsymbol{w}\|^2 \]

距离只跟 \(w\) 的长度有关。这也是为什么我们只要通过控制 \(w\) 的大小,就能控制这条“马路”有多宽。 限制 \(\|w\|\) 不仅仅是为了让马路宽,更是为了防止模型过拟合。马路越宽,模型对噪声的容忍度就越高,大数定律发挥的空间就越大。

求解

再定义新的目标函数,目标函数中要加入对错误分类的点的惩罚

\[L(w, b, \alpha) = \underbrace{\frac{1}{2}\|w\|^2}_{\text{前面:目标}} + \underbrace{\sum_{i=1}^{n} \alpha_i [ 1- y_i(w^Tx_i + b) ]}_{\text{后面:约束}} \]

目标函数追求最小值,w也要追求最小值,a这时候要追求最大值,求解拉格朗日鞍点问题。

在目标函数中,我们追求总代价最小。我们不直接使用“坐标距离”,而是使用松弛变量 \(\xi_i\)。当样本分错时,函数值 \(y_i(\boldsymbol{w}^T \boldsymbol{x}_i + b)\) 确实会小于 1,甚至小于 0。为了抵消这个“负面影响”,我们令 \(\xi_i \ge 1 - y_i(\boldsymbol{w}^T \boldsymbol{x}_i + b)\)。这意味着当分错得越离谱,\(1 - (\text{负数})\) 就会变成一个更大的正数。因此模型在优化过程中会受到强烈的“惩罚”,被迫回头寻找能让 \(\xi_i\) 变小的路径。

软间隔

硬间隔的目标函数基于一种理想假设:数据必须是线性可分的,且不允许任何样本点违背约束。然而在处理真实的 Web 流量数据时,噪声和标注误差不可避免。如果强行追求零错误,分类边界为了适配个别‘离群’的正常样本,会导致特征空间极度扭曲。这不仅增加了模型的复杂度,更会削弱其泛化能力,使得模型在面对未知的攻击变体时反而失去检测效力。

软间隔 SVM 拉格朗日函数

\[L(\boldsymbol{w}, b, \xi, \alpha, \mu) = \underbrace{\frac{1}{2}\|\boldsymbol{w}\|^2 + C \sum_{i=1}^{m} \xi_i}_{\textcircled{1}} + \underbrace{\sum_{i=1}^{m} \alpha_i (1 - \xi_i - y_i(\boldsymbol{w}^T \boldsymbol{x}_i + b))}_{\textcircled{2}} - \underbrace{\sum_{i=1}^{m} \mu_i \xi_i}_{\textcircled{3}} \]

  1. 惩罚系数 \(C\) 决定了模型对分类错误的‘容忍度’。当 \(C\) 趋向于正无穷时,模型退化为硬间隔,任何轻微的分类偏离都会产生巨大的代价值,迫使模型不惜扭曲决策边界也要实现样本的完美划分。反之,若 \(C\) 取值极小,分类错误产生的代价被大幅削弱,模型会变得更加‘佛系’,倾向于牺牲部分样本的准确性来换取一个更平滑、泛化能力更强的分类超平面。
  2. 这套公式实际上是在玩一场‘防作弊’博弈。由于目标函数在求最小值,如果没有 \(-\mu_i \xi_i\) 的约束,模型会试图让 \(\xi_i\) 变成负数来‘刷分’。这个减号项的作用就像是一个单向压力阀:当 \(\xi_i\) 为正时,它在对冲中保持静默;一旦 \(\xi_i\) 越界变负,它就会瞬间触发指数级的惩罚,让整个函数‘爆表’。这种设计不是为了计算距离,而是为了死守 \(\xi_i \ge 0\) 这条底线。

硬间隔,只是软间隔中C为无穷的特殊情况。既然已经不接受任何错误分类,那就不需要写上这一项了。

“拉格朗日函数本质上是一场参数简约性(\(w, b, \xi\)\(\min\)约束严苛性(\(\alpha, \mu\)\(\max\)之间的动态博弈。模型在努力让自己变得简单的同时,裁判在拼命寻找防御漏洞并施加最大惩罚,双方最终在‘支持向量’上达到受力平衡

偏导数

1. 对 \(w\) 求偏导:

\[\frac{\partial L}{\partial \boldsymbol{w}} = \boldsymbol{w} - \sum_{i=1}^{m} \alpha_i y_i \boldsymbol{x}_i = 0 \quad \Rightarrow \quad \mathbf{w = \sum_{i=1}^{m} \alpha_i y_i x_i} \]

  • 物理意义:权重 \(w\) 根本不是什么神秘的东西,它就是所有样本点的加权组合
  • 支持向量的体现:因为大部分 \(\alpha_i\) 都是 0,所以只有那些支持向量真正决定了 \(w\) 的方向。

2. 对 \(b\) 求偏导:

\[\frac{\partial L}{\partial b} = \sum_{i=1}^{m} \alpha_i y_i = 0 \]

  • 物理意义:这代表在最优状态下,所有样本点对分类边界的“推力”之和必须抵消。

3. 对 \(\xi_i\) 求偏导:

这是最精彩的部分,它揭示了你刚才纠结的那个减号的最终归宿:

\[\frac{\partial L}{\partial \xi_i} = C - \alpha_i - \mu_i = 0 \quad \Rightarrow \quad \mathbf{C = \alpha_i + \mu_i} \]

  • 物理意义\(C\)(总罚款)被拆成了两部分:一部分是 \(\alpha_i\)(维持边界的压力),另一部分是 \(\mu_i\)(防止 \(\xi\) 变负的拉力)。
  • 逻辑锁死:因为 \(\mu_i \ge 0\),所以这个等式强制要求了 \(\mathbf{\alpha_i \le C}\)
    上式是一个标准的求极值问题。在最优解处,肯定也满足偏导数这些条件。
    所以可以直接把偏导数带入到上式中。

在最终的对偶目标函数里:因为把需要最小的条件已经删了,所以新的目标函数,就是要求最大值了

\[\max_{\alpha} \quad \underbrace{\sum \alpha_i}_{\text{想变大}} - \underbrace{\frac{1}{2} \sum \sum \alpha_i \alpha_j y_i y_j (x_i \cdot x_j)}_{\text{互相掐架的内耗}} \]

  1. 第一项 \(\sum \alpha_i\):它确实想让权重 \(\alpha\) 越大越好
  2. 第二项(减数):这是“内耗项”。如果两个样本离得太近(内积大),它们如果都想把 \(\alpha\) 变大,这一项就会急剧增加,反而拖累了总分。
  3. 约束 \(0 \le \alpha_i \le C\):这是“天花板”。哪怕你再想变大,最高也只能到 \(C\)

在满足 KKT 条件的约束下,SVM 的训练本质上是在寻找那些‘边界上的异类’。我们并不需要关注所有的样本,而是要找出那些跨越了类别鸿沟、且彼此特征差异最大的异类支持向量。只有当这些代表性样本被赋予最大的权重 \(\alpha\) 时,分类间隔才能被顶向极限,从而构建出鲁棒性最强的防御边界。

KKT约束

1. 偏导为零(Stationarity / 平稳性)

代表在最优解处,函数的“坡度”为 0。

  • 公式 A\(\nabla_w L = w - \sum \alpha_i y_i x_i = 0 \implies \mathbf{w = \sum \alpha_i y_i x_i}\)
  • 公式 B\(\frac{\partial L}{\partial b} = \mathbf{\sum \alpha_i y_i = 0}\)
  • 公式 C\(\frac{\partial L}{\partial \xi_i} = \mathbf{C - \alpha_i - \mu_i = 0}\)

2. 原始可行性(Primal Feasibility / 规矩)

样本点必须落在我们定义的“势力范围”内

  • 公式\(y_i(\mathbf{w}^T \mathbf{x}_i + b) \ge 1 - \xi_i\)\(\xi_i \ge 0\)
  • 理解:这是最基本的物理约束,即样本要么分对,要么乖乖交罚款 \(\xi_i\)

3. 对偶可行性(Dual Feasibility / 裁判立场)

拉格朗日乘子不能是负的。

  • 公式\(\alpha_i \ge 0, \quad \mu_i \ge 0\)
  • 理解:力只能往“把样本推开”的方向使,不能把样本往禁区里拉。

在拉格朗日乘数法中,\(\alpha_i\) 是为了处理不等式约束 \(g(x) \le 0\) 而引入的。

  • 方向一致性:我们的目标是最小化 \(f(w)\)。如果某个约束 \(g(x) \le 0\) 被违反了(即 \(g(x) > 0\)),我们希望拉格朗日函数 \(L = f + \alpha g\) 能够产生一个巨大的正向惩罚
  • 数学锁死:只有当 \(\alpha_i \ge 0\) 时,这个惩罚才是有效的。如果 \(\alpha_i\) 可以是负数,那么当违规发生时(\(g > 0\)),\(\alpha \cdot g\) 就会变成负数,反而帮模型减小了总分。这在逻辑上是荒谬的——相当于“越违规,分越低”。

4. 互补松弛性(Complementary Slackness / 灵魂)

这是最神奇的一条,它决定了谁才是支持向量

  • 公式 A\(\alpha_i (1 - \xi_i - y_i(\mathbf{w}^T \mathbf{x}_i + b)) = 0\)
  • 公式 B\(\mu_i \xi_i = 0\)
  • 直观理解“乘积为 0”意味着两者必有一个为 0
    • 如果点不在边界上(括号不为 0),那么推力 \(\alpha_i\) 必须是 0(不是支持向量)。
    • 如果推力 \(\alpha_i > 0\)(是支持向量),那么点必须正好压在边界上(括号为 0)。
      在拉格朗日目标函数中,若样本点远离边界且分类正确(安全区),其约束项 \((1 - \text{距离})\) 会产生一个巨大的负值。此时,若盲目追求目标函数最小化,模型会产生给该点分配巨大权重 \(\alpha\) 的冲动,试图利用这个负项来“刷低”总能量。

然而,这种“投机行为”直接违背了对偶问题中 \(\max \alpha\) 的核心指令。由于负号的存在,\(\alpha\) 越大,函数值反而越小。为了实现最大化,掌握 \(\alpha\) 生杀大权的裁判会执行“清零政策”:既然你分得太好、不需要惩罚,那就剥夺你的权重。

\(w\) 追求最小与 \(\alpha\) 追求最大的极限拉锯下,双方达成了冷酷的平衡:安全区的样本权重 \(\alpha\) 必须归零。这意味着模型最终实现了解的稀疏性——只有那些“踩在红线上”的支持向量才有资格决定防线的走向。

SMO求解

SMO 的精髓可以用一句话概括:“既然一次性搞不定 10,000 个变量,那我就每次只抓两个出来‘掐架’,直到大家都不再吵架为止。”
** 为什么是两个?**

  • 约束限制:还记得 KKT 条件里的 \(\sum \alpha_i y_i = 0\) 吗?
  • 逻辑闭环:如果你只改一个 \(\alpha_1\),为了维持总和为 0,你必须同时改另一个 \(\alpha_2\) 来抵消变化。所以,两个变量是能进行优化的最小单位

核心目标是解这个 \(\alpha\) 的极大值问题,同时满足两个约束:

  1. 等式约束\(\sum_{i=1}^{n} \alpha_i y_i = 0\)
  2. 边界约束\(0 \le \alpha_i \le C\)

第一步:锁定变量(选择 \(\alpha_1, \alpha_2\)

由于存在等式约束 \(\alpha_1 y_1 + \alpha_2 y_2 = -\sum_{i=3}^{n} \alpha_i y_i = \text{常数}\),我们不能只动一个变量。

  • 选择 \(\alpha_1\):优先选择违反 KKT 条件最严重的样本。
  • 选择 \(\alpha_2\):选择一个能让目标函数值产生最大变化的样本。

第二步:无约束更新

我们要算出 \(\alpha_2\) 的新值 \(\alpha_2^{new}\)。数学上,这是一个抛物线求极值的问题。

1. 计算误差 \(E\)

\[E_i = f(x_i) - y_i \]

\(f(x_i)\) 是模型预测值,\(y_i\) 是真实标签。\(E\) 代表模型预测得有多离谱。)
2. 迭代公式(未修剪):

\[\alpha_2^{new, unt} = \alpha_2^{old} + \frac{y_2(E_1 - E_2)}{\eta} \]

其中,\(\eta = 2\kappa(x_1, x_2) - \kappa(x_1, x_1) - \kappa(x_2, x_2)\) 是学习率的倒数(\(\kappa\) 为核函数)。

直观理解:新权重 = 旧权重 + (标签方向 \(\times\) 误差差距 / 样本相似度)。如果两个样本很像(\(\eta\) 小),步子就会迈得更谨慎。

第三步:修剪与映射(套上枷锁)

因为 \(\alpha\) 必须在 \(0\)\(C\) 之间,且受限于 \(\alpha_1 y_1 + \alpha_2 y_2 = \text{常数}\),所以 \(\alpha_2\) 的取值范围被限制在一个线段 \([L, H]\) 内。
1. 确定上下界 \(L\)\(H\)

  • 如果 \(y_1 \neq y_2\)
    \(L = \max(0, \alpha_2 - \alpha_1), \quad H = \min(C, C + \alpha_2 - \alpha_1)\)
  • 如果 \(y_1 = y_2\)
    \(L = \max(0, \alpha_1 + \alpha_2 - C), \quad H = \min(C, \alpha_1 + \alpha_2)\)
    2. 最终修剪:

\[\alpha_2^{new} = \begin{cases} H & \text{if } \alpha_2^{new, unt} > H \ \alpha_2^{new, unt} & \text{if } L \le \alpha_2^{new, unt} \le H \ L & \text{if } \alpha_2^{new, unt} < L \end{cases}\]

3. 反推 \(\alpha_1\)
根据等式约束,由 \(\alpha_2\) 的变化量算出 \(\alpha_1\),也就是a2增加或者减少的,都原封不动给a1加上。

\[\alpha_1^{new} = \alpha_1^{old} + y_1 y_2 (\alpha_2^{old} - \alpha_2^{new}) \]

求SMO中的b

基于互补松弛性,我们通过解对偶问题得到了权重 \(\alpha\)。其核心价值在于识别出那些恰好落在分类边界上的点(即支持向量),因为只有它们的 \(\alpha > 0\),模型也只需存储这些‘标杆’样本。此时,为了完整定义这道防线在空间的绝对位置,我们还必须利用这些点反向求出偏置项 \(b\)

根据 KKT 条件中的互补松弛性,对于任何一个标准支持向量 \(x_s\)(即对应的 \(0 < \alpha_s < C\)),它必须严格满足函数边界条件:

\[y_s(w^T x_s + b) = 1 \]

由于在 SVM 中,\(y_s\) 的取值只能是 \(+1\)\(-1\),且 \(y_s^2 = 1\),我们可以通过在等式两边同乘 \(y_s\) 来解出 \(b\)

\[b = y_s - w^T x_s \]

在实际中,更喜欢用所有的高权重样本,对b做均值处理。

\[b = \frac{1}{N_s} \sum_{s \in S} \left( y_s - \sum_{j \in SV} \alpha_j y_j \kappa(x_j, x_s) \right) \]

代码

import numpy as np

# 1. 准备 10 个模拟数据点 (5个攻击流量样本, 5个正常流量样本)
# 特征 X: [包长度, 访问频率]
X = np.array([
    [1, 2], [2, 1], [2, 3], [1, 4], [2, 2],  # 类别 -1 (正常)
    [8, 9], [9, 8], [8, 7], [7, 8], [9, 7]   # 类别 +1 (攻击)
], dtype=float)

y = np.array([-1, -1, -1, -1, -1, 1, 1, 1, 1, 1], dtype=float)

class SMOSimulator:
    def __init__(self, C=1.0, tol=1e-3, max_iter=100):
        self.C = C
        self.tol = tol
        self.max_iter = max_iter
        
    def train(self, X, y):
        n_samples, n_features = X.shape
        alpha = np.zeros(n_samples)
        b = 0.0
        
        for iteration in range(self.max_iter):
            changed_alphas = 0
            for i in range(n_samples):
                # 计算预测值 f(xi) = sum(alpha_j * y_j * K(xj, xi)) + b
                f_xi = np.dot((alpha * y), np.dot(X, X[i, :])) + b
                Ei = f_xi - y[i]
                
                # 检查 KKT 条件:如果违反了,就选它作为 alpha_1
                if ((y[i] * Ei < -self.tol and alpha[i] < self.C) or 
                    (y[i] * Ei > self.tol and alpha[i] > 0)):
                    
                    # 随机选择第二个变量 alpha_2
                    j = np.random.choice([idx for idx in range(n_samples) if idx != i])
                    f_xj = np.dot((alpha * y), np.dot(X, X[j, :])) + b
                    Ej = f_xj - y[j]
                    
                    alpha_i_old, alpha_j_old = alpha[i].copy(), alpha[j].copy()
                    
                    # 计算修剪边界 L 和 H
                    if y[i] != y[j]:
                        L = max(0, alpha_j_old - alpha_i_old)
                        H = min(self.C, self.C + alpha_j_old - alpha_i_old)
                    else:
                        L = max(0, alpha_i_old + alpha_j_old - self.C)
                        H = min(self.C, alpha_i_old + alpha_j_old)
                    
                    if L == H: continue
                    
                    # 计算相似度 eta
                    eta = 2.0 * np.dot(X[i], X[j]) - np.dot(X[i], X[i]) - np.dot(X[j], X[j])
                    if eta >= 0: continue
                    
                    # 更新 alpha_j 并修剪
                    alpha[j] -= y[j] * (Ei - Ej) / eta
                    alpha[j] = np.clip(alpha[j], L, H)
                    
                    # 补偿 alpha_i (保持 sum(alpha*y) = 0)
                    alpha[i] += y[i] * y[j] * (alpha_j_old - alpha[j])
                    
                    # 更新偏置 b
                    b1 = b - Ei - y[i]*(alpha[i]-alpha_i_old)*np.dot(X[i],X[i]) - \
                         y[j]*(alpha[j]-alpha_j_old)*np.dot(X[i],X[j])
                    b2 = b - Ej - y[i]*(alpha[i]-alpha_i_old)*np.dot(X[i],X[j]) - \
                         y[j]*(alpha[j]-alpha_j_old)*np.dot(X[j],X[j])
                    
                    if 0 < alpha[i] < self.C: b = b1
                    elif 0 < alpha[j] < self.C: b = b2
                    else: b = (b1 + b2) / 2.0
                    
                    changed_alphas += 1
            
            if changed_alphas == 0: break
            
        return alpha, b

# 运行模拟
model = SMOSimulator(C=0.5)
final_alphas, final_b = model.train(X, y)

print("--- 训练结果 ---")
print(f"最终偏置 b: {final_b:.4f}")
for idx, a in enumerate(final_alphas):
    if a > 1e-5:
        print(f"样本 {idx} 是支持向量, 权重 alpha: {a:.4f}, 坐标: {X[idx]}")
posted @ 2026-03-04 13:47  potatso  阅读(69)  评论(0)    收藏  举报