能分开数据的直线有很多,SVM 为什么选择最大间隔?
从任意分界线对扰动不稳出发,推导硬间隔、软间隔与合页损失,手算 C 的作用,并用 PyTorch 和 scikit-learn 实现可诊断的线性 SVM。
上一篇的 XGBoost 通过许多轴对齐分裂逐步修正预测。树模型能表达复杂边界,但边界由大量局部规则拼成;当特征是高维稀疏向量,或者我们希望得到一条全局、稳定的分界面时,线性模型仍然很有价值。
逻辑回归会用交叉熵学习一条线性边界。不过只要求把训练样本分对时,通常存在许多可行直线。哪一条更不怕测量误差和样本轻微移动?
支持向量机(Support Vector Machine,SVM)的选择是:让分界面离两类中最近的训练样本尽可能远。 本文只讲透最大间隔、软间隔和合页损失这条主线;核技巧留到下一篇。
01 只找到一条分界线为什么不够?#
二分类线性模型先计算决策分数:
- :一个 维样本;
- :边界的法向量;
- :截距;
- :有符号决策分数;
- 标签改写成 ,预测为 。
边界是 。二维中,下面三条线都能分对训练点,但中间那条给两类留下了更大的缓冲带:
x₂
▲ + +
│ + wᵀx+b = +1
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ═══════════════════════════ wᵀx+b = 0
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ wᵀx+b = -1
│ ○ ○
│ ○
└────────────────────────────► x₁
最近的 + / ○ 决定缓冲带宽度text若边界紧贴某个训练点,这个点的特征只要有一点测量误差就会越界。最大间隔(Maximum Margin)不是直接保证未来正确,而是加入一种几何归纳偏置:在训练数据允许的范围内,选择最有余量的边界。
02 函数间隔为什么必须归一化?#
一个样本的有符号函数间隔是:
分对时 ,分错时 。但把 同时乘以 10,边界位置完全不变, 却会放大 10 倍。因此不能只靠增大参数制造“更大间隔”。
点到超平面的几何距离为:
对分对的样本,可写成:
固定缩放,使离边界最近的样本满足 ,两条间隔边界就是:
每一侧到中央边界的距离是 ,完整间隔宽度为:
所以最大化间隔等价于最小化 。
03 硬间隔 SVM 怎样写成优化问题?#
线性可分时,硬间隔支持向量机(Hard-margin SVM)求解:
满足每个训练样本:
这两个部分各司其职:
| 部分 | 约束了什么 | 几何意义 |
|---|---|---|
| 目标尽量小 | 间隔尽量宽 | |
| 所有样本在正确一侧 | 不允许进入间隔或分错 |
刚好满足等号 的点叫支持向量(Support Vector)。它们像撑住两侧间隔的支点;离边界更远的样本约束有余量,轻微移动通常不会改变最优边界。
全部训练样本 X [N,D] + y [N]
│ 求解带约束优化
▼
w [D], b []
│
├── 最近样本:support vectors [S,D]
│
X_query [Q,D] ─► X_query @ w + b ─► scores [Q]
│ sign
▼
labels [Q]text04 一维例子:为什么只有两个点“撑住”边界?#
考虑:
x: [-2, -1, 1, 2]
y: [-1, -1,+1,+1]text取 ,四个函数间隔为:
所有约束都满足。 与 恰好位于 和 ,是支持向量; 与 更远。
边界在 ,完整间隔宽度为:
如果把远点从 改成 ,最近的两个点不变,最优边界也不变。这是“支持向量”名称最直接的含义:最终解主要由靠近边界的困难样本支撑,而不是由每个远处样本平均决定。
交互手算:把正类最近点从 1 移到 3
最近负类仍是 ,最近正类变成 。对称的最大间隔边界移动到 ,两侧支持向量到边界的距离都是 2,完整间隔变为 4。远处点只要没有成为新的最近点,就不会撑住边界。
05 一个异常点为何让硬间隔失效?#
真实数据常有重叠和错标。若在上例加入一个负类点 ,就无法用原来的宽间隔把所有点都放到正确区域;更极端时,两类完全交错,硬间隔问题没有可行解。
软间隔支持向量机(Soft-margin SVM)为每个样本加入松弛变量(Slack Variable):
约束改成:
松弛量可以直接读成违反间隔的程度:
| 最小 | 状态 | |
|---|---|---|
| 0 | 正确且在间隔外 | |
| 0 | 位于间隔边界,支持向量 | |
| 分对但进入间隔 | ||
| 分错或恰在决策边界 |
是违反间隔的价格:大 更努力减少训练违规,可能得到窄而曲折的边界;小 更愿意容忍违规,以换取更小的 和更宽的间隔。
06 合页损失把约束变成可优化目标#
对固定 ,最小松弛量就是合页损失(Hinge Loss):
软间隔目标可写成:
hinge loss
▲
│\
│ \
│ \
│ \__________
└───────────────► margin m = y f(x)
0 1
m < 1:仍有线性梯度
m > 1:损失为 0,不再推动远处样本text这与逻辑回归的交叉熵很不一样。交叉熵会继续奖励更高的正确置信度;标准合页损失在样本越过 后变成 0。SVM 因而把优化重点放在间隔内和边界附近的点。
手算 C 如何改变选择#
仍用五个样本:
x = [-2,-1,0.2,1,2]
y = [-1,-1, -1,+1,+1]text比较两个无截距候选模型:
| 候选 | 合页损失之和 | 的目标 | 的目标 | |
|---|---|---|---|---|
| A: | 0.500 | 1.200 | 1.700 | 0.620 |
| B: | 0.125 | 2.100 | 2.225 | 0.335 |
时,A 用更大权重换来较少违规; 时,B 的更宽间隔更值得,哪怕更多样本进入间隔。真实训练不是只在两个候选间选择,但权衡完全相同。
07 不依赖黑盒:用 PyTorch 写出线性软间隔#
下面直接优化“权重平方 + 平均合页损失”。为了让偏置不被正则化,显式拆出 weight 和 bias:
import torch
torch.manual_seed(42)
X = torch.tensor([
[-2.0, -1.0],
[-1.0, -1.0],
[-0.5, 0.2],
[ 0.8, 1.0],
[ 2.0, 1.0],
]) # [N=5, D=2]
y = torch.tensor([-1.0, -1.0, -1.0, 1.0, 1.0]) # [5]
weight = torch.zeros(2, requires_grad=True) # [D]
bias = torch.zeros((), requires_grad=True) # []
optimizer = torch.optim.SGD([weight, bias], lr=0.05)
C = 1.0
for step in range(1000):
score = X @ weight + bias # [N]
margin = y * score # [N]
per_sample_hinge = torch.relu(1.0 - margin) # [N]
regularizer = 0.5 * weight.square().sum() # []
objective = regularizer + C * per_sample_hinge.mean()
optimizer.zero_grad(set_to_none=True)
objective.backward()
optimizer.step()
with torch.no_grad():
query = torch.tensor([[0.0, 0.5], [1.5, 0.0]]) # [Q=2,D=2]
query_score = query @ weight + bias # [2]
prediction = torch.where(query_score >= 0, 1, -1)python关键调试量不是只有准确率:
with torch.no_grad():
margin = y * (X @ weight + bias)
print('weight norm:', weight.norm().item())
print('minimum margin:', margin.min().item())
print('inside margin:', (margin <= 1.0).sum().item())
print('misclassified:', (margin <= 0.0).sum().item())python本文代码对合页损失取均值,而经典 -SVM 常写成求和。因此这里的 C=1 不等于任意库中的同名数值;比较实验必须固定样本数、缩放方式和目标约定。
08 用 scikit-learn 1.9 正确落地#
若希望直接检查支持向量,可用 SVC(kernel='linear')。SVM 对尺度不具不变性,标准化必须放进 Pipeline,避免交叉验证泄漏:
import numpy as np
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
pipeline = make_pipeline(
StandardScaler(),
SVC(kernel='linear'),
)
search = GridSearchCV(
estimator=pipeline,
param_grid={'svc__C': np.logspace(-3, 3, 13)},
scoring='balanced_accuracy',
cv=StratifiedKFold(n_splits=5, shuffle=True, random_state=42),
n_jobs=-1,
refit=True,
)
search.fit(X_train, y_train) # X_train [N,D], y_train [N]
scores = search.decision_function(X_val) # [num_val]
labels = search.predict(X_val) # [num_val]
svc = search.best_estimator_.named_steps['svc']
print('classes:', svc.classes_)
print('support indices:', svc.support_) # [S]
print('support vectors:', svc.support_vectors_.shape) # [S,D]
print('weight shape:', svc.coef_.shape) # [1,D]python注意 support_vectors_ 位于标准化后的空间,因为 SVC 接收的是 Pipeline 上一步输出。原始训练样本索引可用 support_ 回到 X_train。
当样本很多且只需要线性边界,LinearSVC 通常更可扩展:
from sklearn.svm import LinearSVC
linear_pipeline = make_pipeline(
StandardScaler(),
LinearSVC(
C=1.0,
loss='hinge',
penalty='l2',
dual='auto',
max_iter=10_000,
random_state=42,
),
)
linear_pipeline.fit(X_train, y_train)pythonscikit-learn 1.9 中,LinearSVC 默认损失其实是 squared_hinge;上面显式设为 hinge 才与本文公式一致。它由 liblinear 实现,规模通常优于 SVC(kernel='linear'),但不提供 support_;二者的损失默认值和截距正则化也不同,不能期待逐点完全相同。
09 工程中最常见的错误与调试路径#
- 没有缩放特征。 若收入数值是年龄的几千倍, 与几何距离会被单位扭曲。只在训练折拟合
StandardScaler。 - 把分数当概率。 SVM 原生输出间隔分数,不在 。需要概率时应在独立交叉验证框架中校准,而不是手动 Sigmoid。
- 误解
C。 大C更在乎训练违规,小C正则更强;画验证曲线并同时记录支持向量比例。 - 用
LinearSVC查support_。 它不保存 libsvm 意义下的支持向量;需要这些属性时用SVC(kernel='linear')。 - 标签仍是 0/1 却手写 。 数学推导要求 ;代码里先显式映射并断言。
- 只加大
max_iter。 出现收敛警告时先检查尺度、重复特征、异常值和tol,再提高迭代上限。 - 类别失衡只看准确率。 检查每类召回率和间隔分布;
class_weight='balanced'会按类别缩放有效 ,仍需验证。
最小不变量:
assert X_train.ndim == 2 and y_train.ndim == 1
assert X_train.shape[0] == y_train.shape[0]
assert np.isfinite(X_train).all()
assert scores.shape == (X_val.shape[0],)
assert np.isfinite(scores).all()
print('best C:', search.best_params_['svc__C'])
print('support ratio:', len(svc.support_) / len(X_train))python支持向量比例非常高,可能说明类别重叠严重、特征噪声大、C 选择不合适,或线性边界根本不匹配;它不是一个单独的质量指标。
10 它什么时候失败?与逻辑回归有什么不同?#
| 方法 | 训练目标 | 远离边界的正确样本 | 原生概率 | 线性边界 |
|---|---|---|---|---|
| 线性 SVM | 权重范数 + 合页损失 | 超过间隔后损失为 0 | 否 | 是 |
| 逻辑回归 | L2 等正则 + 对数损失 | 仍有很小损失 | 是,但仍需校准检查 | 是 |
| 感知机 | 错分样本触发更新 | 分对后不管间隔 | 否 | 是 |
| 决策树 | 不纯度贪心分裂 | 不适用 | 叶频率 | 轴对齐分段 |
线性 SVM 会在以下场景失效或失去优势:
- 原始空间边界明显弯曲,例如同心圆;
- 标签严重重叠或错标,支持向量过多,边界对困难点敏感;
- 业务必须直接得到可靠概率,却没有足够校准数据;
- 数据分布漂移使旧支持向量不再代表新边界;
- 特征量纲、缺失值或线上预处理与训练不一致。
最大间隔提升的是特定假设空间下的稳健性,不会修复数据泄漏、错误标签或缺失特征。
11 今天真正需要记住什么?#
- 线性 SVM 在所有可行边界中选择几何间隔最大的一个;间隔宽度由 给出。
- 支持向量位于间隔边界内或附近,它们支撑最优解;远处样本越过间隔后不再产生合页损失。
- 软间隔用松弛变量容忍重叠与噪声,
C在小权重和少违规之间权衡。 - 合页损失 让“分对但太靠近边界”的样本仍参与训练。
- SVM 必须重视尺度、数据划分和分数语义;
decision_function不是概率。
12 思考题与小练习#
练习 1:判断三个点的状态
设 分别为 1.4、0.6、-0.2。对应合页损失是 0、0.4、1.2;第一个在间隔外,第二个分对但进入间隔,第三个分错。
练习 2:从参数得到几何宽度
若 ,则 。中央边界到任一间隔面的距离是 ,完整间隔宽度是 。把 同时乘 2 不改变边界,却不再满足本文固定的规范化约束。
练习 3:画 C 的验证曲线
在 Pipeline 中比较 C=10^{-3},...,10^3,记录训练/验证平衡准确率、 和支持向量比例。解释为何训练分数通常随 C 增大而改善,验证分数却可能先升后降。
相关工作#
- Cortes & Vapnik: Support-Vector Networks ↗:软间隔支持向量网络的奠基论文。
- Boser, Guyon & Vapnik: A Training Algorithm for Optimal Margin Classifiers ↗:最大间隔与核方法的重要早期工作。
- Platt: Sequential Minimal Optimization ↗:高效求解 SVM 对偶问题的经典算法。
- LIBSVM: A Library for Support Vector Machines ↗:libsvm 的算法与工程实现论文。
- scikit-learn: Support Vector Machines ↗:当前数学形式、复杂度、缩放与实现差异说明。
13 下一篇预告#
线性软间隔能容忍少量重叠,却仍只能画一条超平面。下一篇将把点积替换成核函数,手算 RBF 相似度如何让支持向量拼出弯曲边界,并看清 gamma 与 C 为什么必须联合选择。