机器学习分类算法三——SVM
一、算法原理
支持向量机(Support Vector Machine, SVM)是一种经典的监督学习算法,主要用于分类和回归任务。其核心思想是找到一个最优超平面,使得不同类别的数据点之间的间隔(Margin)最大化,从而提高模型的泛化能力。
1. SVM 的基本思想
(1) 线性可分情况
假设数据是线性可分的(即可以用一条直线或超平面完全分开),SVM 的目标是找到一个决策边界(超平面),使得:
正确分类所有训练样本。
最大化分类间隔(即两类数据点到超平面的最小距离之和)。
数学表示:
给定训练数据:X={(xi,yi)}i=1n,其中xi∈Rd是特征向量,yi∈{−1,+1}是类别标签X=\{(x_i,y_i)\}_{i=1}^n,其中x_i \in R^d是特征向量,y_i\in\{-1,+1\}是类别标签X={(xi,yi)}i=1n,其中xi∈Rd是特征向量,yi∈{−1,+1}是类别标签
超平面方程为:
wtx+b=0\bm{w^tx}+b=0wtx+b=0
其中,w是法向量,b是偏置。\bm{w}是法向量,b是偏置。w是法向量,b是偏置。
分类间隔
对于任意样本,到超平面的距离为:
di=∣wtxi+b∣∣∣w∣∣d_i =\frac{|\bm{w_tx_i+b}|}{||\bm{w}||}di=∣∣w∣∣∣wtxi+b∣
由于yi∈{−1,+1}y_i \in\{-1,+1\}yi∈{−1,+1},则:
di=yi(wtxi+b)∣∣w∣∣d_i =\frac{y_i(\bm{w_tx_i}+b)}{||\bm{w}||}di=∣∣w∣∣yi(wtxi+b)
分类间隔(Margin)定义为所有样本到超平面的最小距离的两倍:
Magin=2∣∣w∣∣Magin=\frac{2}{||\bm{w}||}Magin=∣∣w∣∣2
优化目标
为了最大化间隔,SVM 的优化问题可以表示为:
minw,b12∣∣w∣∣2,s.t.yi(wtxi+b)≥1,∀i\mathop{\min}_{\bm{w},b}\frac{1}{2}||\bm{w}||^2, s.t. y_i(\bm{w_tx_i}+b) \geq 1 ,\forall iminw,b21∣∣w∣∣2,s.t.yi(wtxi+b)≥1,∀i
即:
1、最小化∣∣w∣∣||\bm{w}||∣∣w∣∣等价于最大化MarginMarginMargin。
2、确保所有样本正确分类(约束条件)。
损失函数:
铰链损失(Hinge Loss)函数:
HingeLoss=max(0,1−yi(wiTxi+b))HingeLoss =\max(0,1-y_i(\bm{w_i^Tx_i}+b))HingeLoss=max(0,1−yi(wiTxi+b))
优化方法:随机梯度下降(SGD)和 序列最小优化算法(SMO)
SGD(随机梯度下降)
用途:通用优化算法,用于最小化任意可微的损失函数(如线性回归、逻辑回归、神经网络等)。
目标:通过迭代更新参数(如权重 w和偏置b),逐步逼近损失函数的全局最小值。
适用场景:大规模数据集、在线学习(数据流式到达)。
SMO(序列最小优化算法)
用途:专门用于解决支持向量机(SVM)的对偶问题(即优化拉格朗日乘子 α)。
目标:快速求解 SVM 的二次规划(QP)问题,找到最优的 α向量(满足 KKT 条件)。
适用场景:中小规模数据集(SVM 的训练通常需要存储核矩阵,内存消耗较大)。
(2)软间隔 SVM(处理非线性可分数据)
实际数据往往不是完全线性可分的(可能存在噪声或异常点)。为了允许少量误分类,引入松弛变量(Slack Variables) ξi≥0:\xi_i \geq 0:ξi≥0:
minw,b,ξ12∣∣w∣∣2+C∑i=1nξi, s.t.yi(wtxi+b)≥1−ξi,ξi≥0,∀i\mathop{\min}_{\bm{w},b,\xi}\frac{1}{2}||\bm{w}||^2+C\sum^n_{i=1}\xi_i, \ \ s.t. y_i(\bm{w_tx_i}+b) \geq 1-\xi_i ,\xi_i \geq 0,\forall iminw,b,ξ21∣∣w∣∣2+C∑i=1nξi, s.t.yi(wtxi+b)≥1−ξi,ξi≥0,∀i
其中:C>0是正则化参数,控制对误分类的惩罚程度。
C越大,模型越严格(不允许误分类)。
C越小,模型越宽松(允许更多误分类)。
2. 对偶问题与核方法
(1) 拉格朗日对偶问题
原始优化问题可以通过拉格朗日乘子法转化为对偶问题,便于求解和引入核函数。
对偶问题的形式为:
maxα∑i=1nαi−12∑i=1n∑j=1nαiαjyiyjxiTxj, s.t. ∑i=1naiyi=0,0≤ai≤C,∀i\max \limits_{\alpha}\sum_{i=1}^n\alpha_i-\frac{1}{2}\sum_{i=1}^n\sum_{j=1}^n\alpha_i\alpha_jy_iy_j\bm{x_i^Tx_j}, \ \ s.t. \ \sum_{i=1}^n a_iy_i=0,0 \leq a_i\leq C,\forall iαmax∑i=1nαi−21∑i=1n∑j=1nαiαjyiyjxiTxj, s.t. ∑i=1naiyi=0,0≤ai≤C,∀i
其中,αi\alpha_iαi是拉格朗日乘子(对应原始问题的约束)。
只用αi>0\alpha_i>0αi>0的样本是支持向量(Support Vectors),它们决定了超平面的位置。
决策函数
最终分类决策函数为:
f(x)=sign(∑i=1naiyixiTx+b)f(x)=sign(\sum_{i=1}^n a_iy_i\bm{x_i^Tx}+b)f(x)=sign(∑i=1naiyixiTx+b)
3、核方法(Kernel Trick)
当数据非线性可分时,可以通过核函数(Kernel Function)将数据映射到更高维的特征空间,使其线性可分。
常见的核函数包括:
(1)线性核(Linear Kernel):
K(xi,xj)=xiTxjK(\bm{x_i},\bm{x_j})=\bm{x_i^Tx_j}K(xi,xj)=xiTxj
(2)多项式核(Polynomial Kernel):
K(xi,xj)=(xiTxj+c)dK(\bm{x_i},\bm{x_j})=(\bm{x_i^Tx_j}+c)^dK(xi,xj)=(xiTxj+c)d
(3)高斯核(RBF Kernel):
K(xi,xj)=exp(−γ∣∣xi−xj∣∣2)K(\bm{x_i},\bm{x_j})=exp(-\gamma||\bm{x_i-x_j}||^2)K(xi,xj)=exp(−γ∣∣xi−xj∣∣2)
其中γ>0\gamma >0γ>0控制高斯分布的宽度。
核化决策函数
使用核函数后,决策函数变为:
f(x)=sign(∑i=1naiyiK(xi,x)+b)f(x)=sign(\sum_{i=1}^n a_iy_iK(\bm{x_i,x})+b)f(x)=sign(∑i=1naiyiK(xi,x)+b)
4、序列最小优化(SMO, Sequential Minimal Optimization)
(1)每次只优化两个拉格朗日乘子 αi\alpha_iαi和αj\alpha_jαj(因为∑αiyi=0的约束。\sum \alpha_i y_i=0的约束。∑αiyi=0的约束。)
(2)固定其他的αk(k≠i,j)\alpha_k(k\neq i,j)αk(k=i,j),求解这两个变量的优化问题。
(3)重复这两个步骤,直到所有αk\alpha_kαk满足KKT 条件(最优性条件)。
二、参考代码
class SVMSMO:
def __init__(self, C=1.0, kernel='rbf', gamma='auto', tol=1e-3, max_iter=100, cache_size=100):
self.C = C
self.kernel = kernel
self.gamma = gamma
self.tol = tol
self.max_iter = max_iter
self.cache_size = cache_size
self.alpha = None
self.b = 0
self.X = None
self.y = None
self.support_vectors = None
self.support_alpha = None
self.support_y = None
self.kernel_cache = {} # 核函数缓存
self.E_cache = {} # 误差缓存
def _kernel(self, x1, x2):
"""计算核函数(线性核或 RBF 核),使用缓存优化"""
# 生成唯一键(基于数据内容而非内存地址)
key = (tuple(x1), tuple(x2)) if hasattr(x1, '__len__') else (x1, x2)
if key in self.kernel_cache:
return self.kernel_cache[key]
if self.kernel == 'linear':
result = np.dot(x1, x2)
elif self.kernel == 'rbf':
if self.gamma == 'auto':
self.gamma = 1.0 / self.X.shape[1] if self.X is not None else 1.0
result = np.exp(-self.gamma * np.linalg.norm(x1 - x2) ** 2)
else:
raise ValueError("Unsupported kernel")
# 限制缓存大小
if len(self.kernel_cache) < self.cache_size:
self.kernel_cache[key] = result
return result
def _compute_E(self, i):
"""计算预测误差 E_i = f(x_i) - y_i,使用缓存优化"""
if i in self.E_cache:
return self.E_cache[i]
f_xi = 0.0
for j in range(self.X.shape[0]):
f_xi += self.alpha[j] * self.y[j] * self._kernel(self.X[j], self.X[i])
f_xi += self.b
E_i = f_xi - self.y[i]
# 缓存误差
if len(self.E_cache) < self.cache_size:
self.E_cache[i] = E_i
return E_i
def _violate_kkt(self, i, E_i):
"""检查样本i是否违反KKT条件"""
return ((self.y[i] * E_i < -self.tol and self.alpha[i] < self.C) or
(self.y[i] * E_i > self.tol and self.alpha[i] > 0))
def _select_j(self, i, E_i):
"""选择第二个变量j(使|E_i - E_j|最大),使用误差缓存"""
max_diff = -1
best_j = -1
E_j = 0
# 优先考虑alpha>0的样本(支持向量候选)
candidates = [idx for idx in range(self.X.shape[0]) if idx != i and self.alpha[idx] > 1e-5]
if not candidates:
candidates = [idx for idx in range(self.X.shape[0]) if idx != i]
for j in candidates:
E_j_tmp = self._compute_E(j)
diff = abs(E_i - E_j_tmp)
if diff > max_diff:
max_diff = diff
best_j = j
E_j = E_j_tmp
return best_j, E_j
def _update_alpha(self, i, j):
"""更新alpha_i和alpha_j,优化核函数计算"""
if i == j:
return False
x_i, y_i, alpha_i = self.X[i], self.y[i], self.alpha[i]
x_j, y_j, alpha_j = self.X[j], self.y[j], self.alpha[j]
E_i, E_j = self._compute_E(i), self._compute_E(j)
# 计算边界L和H
if y_i != y_j:
L, H = max(0, alpha_j - alpha_i), min(self.C, self.C + alpha_j - alpha_i)
else:
L, H = max(0, alpha_i + alpha_j - self.C), min(self.C, alpha_i + alpha_j)
if L == H:
return False
# 计算eta(二次项系数)
eta = 2 * self._kernel(x_i, x_j) - self._kernel(x_i, x_i) - self._kernel(x_j, x_j)
if eta >= 0:
return False
# 更新alpha_j和alpha_i
new_alpha_j = alpha_j - y_j * (E_i - E_j) / eta
new_alpha_j = np.clip(new_alpha_j, L, H)
if abs(new_alpha_j - alpha_j) < 1e-5:
return False
new_alpha_i = alpha_i + y_i * y_j * (alpha_j - new_alpha_j)
self.alpha[i], self.alpha[j] = new_alpha_i, new_alpha_j
# 更新偏置b
b1 = self.b - E_i - y_i * (new_alpha_i - alpha_i) * self._kernel(x_i, x_i) - y_j * (new_alpha_j - alpha_j) * self._kernel(x_i, x_j)
b2 = self.b - E_j - y_i * (new_alpha_i - alpha_i) * self._kernel(x_i, x_j) - y_j * (new_alpha_j - alpha_j) * self._kernel(x_j, x_j)
if 0 < new_alpha_i < self.C:
self.b = b1
elif 0 < new_alpha_j < self.C:
self.b = b2
else:
self.b = (b1 + b2) / 2
return True
def fit(self, X, y):
"""训练SVM"""
self.X = np.array(X)
self.y = np.array(y).flatten()
n_samples = self.X.shape[0]
self.alpha = np.zeros(n_samples)
self.b = 0
self.kernel_cache = {}
self.E_cache = {}
for _ in range(self.max_iter):
num_changed_alphas = 0
for i in range(n_samples):
E_i = self._compute_E(i)
if self._violate_kkt(i, E_i):
j, E_j = self._select_j(i, E_i)
if j != -1 and self._update_alpha(i, j):
num_changed_alphas += 1
if num_changed_alphas == 0:
break
# 保存支持向量
sv_idx = self.alpha > 1e-5
self.support_vectors = self.X[sv_idx]
self.support_alpha = self.alpha[sv_idx]
self.support_y = self.y[sv_idx]
def predict(self, X):
"""预测"""
X = np.array(X)
if X.ndim == 1:
X = X.reshape(1, -1)
y_pred = np.zeros(X.shape[0])
for i in range(X.shape[0]):
prediction = 0
for alpha, sv_y, sv in zip(self.support_alpha, self.support_y, self.support_vectors):
prediction += alpha * sv_y * self._kernel(X[i], sv)
y_pred[i] = prediction + self.b
return np.sign(y_pred)
def generate_svm_data(self, n_samples=100, dataset_type="linear", noise=0.1, random_state=42):
"""生成数据(仅返回 X, y,不划分训练集/测试集)"""
np.random.seed(random_state)
if dataset_type == "linear":
X, y = make_blobs(n_samples=n_samples, centers=2, cluster_std=0.5, random_state=random_state)
elif dataset_type == "moons":
X, y = make_moons(n_samples=n_samples, noise=noise, random_state=random_state)
elif dataset_type == "circles":
X, y = make_circles(n_samples=n_samples, noise=noise, factor=0.4, random_state=random_state)
else:
raise ValueError("dataset_type mustbe 'linear', 'moons' or 'circles'")
y = np.where(y == 0, -1, 1)
X += np.random.normal(scale=noise, size=X.shape)
return X, y
def plot_decision_boundary(self, X, y, title):
# 创建网格
x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5
y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
# 预测网格点
Z = self.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
# 绘图
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, Z, cmap='coolwarm', alpha=0.3)
plt.scatter(self.support_vectors[:, 0], self.support_vectors[:, 1],
facecolors='none', edgecolors='m', s=20, linewidths=2)
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='viridis', edgecolors='k', s=50)
plt.title(title)
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.show()
三、应用场景和优缺点
1、SVM 的应用场景
图像分类(如手写数字识别)。
文本分类(如垃圾邮件检测)。
生物信息学(如基因分类)。
金融风控(如信用评分)。
医学诊断(如疾病预测)。
2、优点
高维数据有效:核方法可以处理高维数据。
泛化能力强:最大化间隔的策略减少了过拟合风险。
适用于小样本:即使样本较少,也能找到最优超平面。
理论保证:有严格的数学理论支持(如 VC 维理论)。
3、缺点
计算复杂度高:训练时间复杂度为
O(n3n^3n3)(SMO 优化后有所改善)。
对参数敏感:核函数参数(如 γ)和正则化参数 C需要调优。
难以处理大规模数据:内存消耗大(需要存储核矩阵)。
对噪声敏感:软间隔 SVM 可以缓解,但仍可能受极端值影响。
四、SMO和SGD方法比较
1、线性数据(n=500,n_features=2)
SGDSVM参数:learning_rate=0.001, lambda_param=0.01, n_iters=1000
准确率:100%。
SMOSVM参数kernel=‘linear’, C=100.0,max_iter=10000
准确率:97.00%。
2、非线性数据
SGDSVM参数:learning_rate=0.001, lambda_param=0.01, n_iters=1000
准确率:62.00%,基本上不能应用。
SMOSVM参数:kernel=‘rbf’, C=100.0,max_iter=10000
准确率:92%,正常使用。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)