SVM支持向量机
如果给定的一堆向量无法区分,那就给他们通过某种程度升维,人为再造出一个维度。可以理解为创造的这个线性相关(但其实不是线性相关)的矩阵,就恰好能区分数据集。
人为给数据升维的方式成为核函数
数学推导
为了方便,我们假定二分类数据,是以0这个超平面对称分布。在二分类数据集画两条线,尽量让线中间的距离增大。这样尽可能区分1和-1数据。
- 线 A (恶意侧):\(w^T x + b = 1\)
- 线 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的和,根据点到直线的距离公式,距离为
我们的目标是
在数学上,最大化一个正的分式 \(\frac{2}{\|\boldsymbol{w}\|}\),完全等价于最小化它的倒数 \(\frac{\|\boldsymbol{w}\|}{2}\)。
因为模长带有根号,在一会的拉格朗日数乘法计算过程中会十分复杂。
为了让数学过程变得“优雅”且易于计算:
- 平方处理:最小化 \(\|\boldsymbol{w}\|\) 等价于最小化 \(\|\boldsymbol{w}\|^2\)。平方之后,根号消失了,变成了一个平滑的二次函数。
- 乘以 1/2:在前面加一个 \(\frac{1}{2}\) 是为了在求导时,把平方项落下来的那个 \(2\) 给正好抵消掉(即 \(( \frac{1}{2} w^2 )' = w\))。
于是,最终的目标函数为:
距离只跟 \(w\) 的长度有关。这也是为什么我们只要通过控制 \(w\) 的大小,就能控制这条“马路”有多宽。 限制 \(\|w\|\) 不仅仅是为了让马路宽,更是为了防止模型过拟合。马路越宽,模型对噪声的容忍度就越高,大数定律发挥的空间就越大。
求解
再定义新的目标函数,目标函数中要加入对错误分类的点的惩罚
目标函数追求最小值,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 拉格朗日函数
- 惩罚系数 \(C\) 决定了模型对分类错误的‘容忍度’。当 \(C\) 趋向于正无穷时,模型退化为硬间隔,任何轻微的分类偏离都会产生巨大的代价值,迫使模型不惜扭曲决策边界也要实现样本的完美划分。反之,若 \(C\) 取值极小,分类错误产生的代价被大幅削弱,模型会变得更加‘佛系’,倾向于牺牲部分样本的准确性来换取一个更平滑、泛化能力更强的分类超平面。
- 这套公式实际上是在玩一场‘防作弊’博弈。由于目标函数在求最小值,如果没有 \(-\mu_i \xi_i\) 的约束,模型会试图让 \(\xi_i\) 变成负数来‘刷分’。这个减号项的作用就像是一个单向压力阀:当 \(\xi_i\) 为正时,它在对冲中保持静默;一旦 \(\xi_i\) 越界变负,它就会瞬间触发指数级的惩罚,让整个函数‘爆表’。这种设计不是为了计算距离,而是为了死守 \(\xi_i \ge 0\) 这条底线。
硬间隔,只是软间隔中C为无穷的特殊情况。既然已经不接受任何错误分类,那就不需要写上这一项了。
“拉格朗日函数本质上是一场参数简约性(\(w, b, \xi\) 的 \(\min\))与约束严苛性(\(\alpha, \mu\) 的 \(\max\))之间的动态博弈。模型在努力让自己变得简单的同时,裁判在拼命寻找防御漏洞并施加最大惩罚,双方最终在‘支持向量’上达到受力平衡
偏导数
1. 对 \(w\) 求偏导:
- 物理意义:权重 \(w\) 根本不是什么神秘的东西,它就是所有样本点的加权组合。
- 支持向量的体现:因为大部分 \(\alpha_i\) 都是 0,所以只有那些支持向量真正决定了 \(w\) 的方向。
2. 对 \(b\) 求偏导:
- 物理意义:这代表在最优状态下,所有样本点对分类边界的“推力”之和必须抵消。
3. 对 \(\xi_i\) 求偏导:
这是最精彩的部分,它揭示了你刚才纠结的那个减号的最终归宿:
- 物理意义:\(C\)(总罚款)被拆成了两部分:一部分是 \(\alpha_i\)(维持边界的压力),另一部分是 \(\mu_i\)(防止 \(\xi\) 变负的拉力)。
- 逻辑锁死:因为 \(\mu_i \ge 0\),所以这个等式强制要求了 \(\mathbf{\alpha_i \le C}\)。
上式是一个标准的求极值问题。在最优解处,肯定也满足偏导数这些条件。
所以可以直接把偏导数带入到上式中。
在最终的对偶目标函数里:因为把需要最小的条件已经删了,所以新的目标函数,就是要求最大值了
- 第一项 \(\sum \alpha_i\):它确实想让权重 \(\alpha\) 越大越好。
- 第二项(减数):这是“内耗项”。如果两个样本离得太近(内积大),它们如果都想把 \(\alpha\) 变大,这一项就会急剧增加,反而拖累了总分。
- 约束 \(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\) 的极大值问题,同时满足两个约束:
- 等式约束:\(\sum_{i=1}^{n} \alpha_i y_i = 0\)
- 边界约束:\(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\):
(\(f(x_i)\) 是模型预测值,\(y_i\) 是真实标签。\(E\) 代表模型预测得有多离谱。)
2. 迭代公式(未修剪):
其中,\(\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. 最终修剪:
3. 反推 \(\alpha_1\):
根据等式约束,由 \(\alpha_2\) 的变化量算出 \(\alpha_1\),也就是a2增加或者减少的,都原封不动给a1加上。
求SMO中的b
基于互补松弛性,我们通过解对偶问题得到了权重 \(\alpha\)。其核心价值在于识别出那些恰好落在分类边界上的点(即支持向量),因为只有它们的 \(\alpha > 0\),模型也只需存储这些‘标杆’样本。此时,为了完整定义这道防线在空间的绝对位置,我们还必须利用这些点反向求出偏置项 \(b\)。
根据 KKT 条件中的互补松弛性,对于任何一个标准支持向量 \(x_s\)(即对应的 \(0 < \alpha_s < C\)),它必须严格满足函数边界条件:
由于在 SVM 中,\(y_s\) 的取值只能是 \(+1\) 或 \(-1\),且 \(y_s^2 = 1\),我们可以通过在等式两边同乘 \(y_s\) 来解出 \(b\):
在实际中,更喜欢用所有的高权重样本,对b做均值处理。
代码
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]}")
浙公网安备 33010602011771号