支持向量机对偶问题求解:从数学原理到Python实现

1次阅读
没有评论

共计 4545 个字符,预计需要花费 12 分钟才能阅读完成。

image.webp

1. SVM 原始问题与对偶问题转换

支持向量机 (SVM) 最初的形式是一个凸二次规划问题,其原始优化目标为:

支持向量机对偶问题求解:从数学原理到 Python 实现

$$
\min_{w,b} \frac{1}{2}||w||^2 \quad \text{s.t.} \quad y_i(w^Tx_i + b) \geq 1, \forall i
$$

为了将这个带约束的优化问题转化为无约束问题,我们引入拉格朗日乘子法。构建拉格朗日函数:

$$
L(w,b,\alpha) = \frac{1}{2}||w||^2 – \sum_{i=1}^n \alpha_i[y_i(w^Tx_i + b) – 1]
$$

其中 $\alpha_i \geq 0$ 是拉格朗日乘子。根据 KKT 条件,最优解需要满足:

  1. 梯度为零:$\frac{\partial L}{\partial w} = 0$ 和 $\frac{\partial L}{\partial b} = 0$
  2. 原始可行性:$y_i(w^Tx_i + b) \geq 1$
  3. 对偶可行性:$\alpha_i \geq 0$
  4. 互补松弛性:$\alpha_i[y_i(w^Tx_i + b) – 1] = 0$

通过求解梯度为零的条件,我们得到:

$$
w = \sum_{i=1}^n \alpha_i y_i x_i
$$

$$
\sum_{i=1}^n \alpha_i y_i = 0
$$

将这两个关系式代回拉格朗日函数,就得到了对偶问题:

$$
\max_{\alpha} \sum_{i=1}^n \alpha_i – \frac{1}{2} \sum_{i,j=1}^n \alpha_i \alpha_j y_i y_j x_i^T x_j
$$

$$
\text{s.t.} \quad \sum_{i=1}^n \alpha_i y_i = 0, \alpha_i \geq 0
$$

2. 原始问题与对偶问题计算复杂度对比

  • 原始问题:需要优化 $w \in \mathbb{R}^d$ 和 $b \in \mathbb{R}$,变量数量与特征维度 $d$ 相关
  • 对偶问题:需要优化 $\alpha \in \mathbb{R}^n$,变量数量与样本数量 $n$ 相关

当特征维度 $d$ 远大于样本数量 $n$ 时,对偶问题更高效。此外,对偶形式天然支持核技巧,可以通过核函数 $K(x_i,x_j)$ 隐式映射到高维空间。

3. Python 实现

3.1 使用 numpy 实现

import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_classification
import matplotlib.pyplot as plt

# 生成数据
X, y = make_classification(n_samples=100, n_features=2, n_redundant=0, 
                          n_clusters_per_class=1, random_state=42)
y[y==0] = -1  # 将标签转换为 - 1 和 1

# 数据标准化
scaler = StandardScaler()
X = scaler.fit_transform(X)

# 核函数实现
def linear_kernel(x1, x2):
    return np.dot(x1, x2)

def rbf_kernel(x1, x2, gamma=0.1):
    return np.exp(-gamma * np.linalg.norm(x1 - x2)**2)

# SVM 对偶问题求解
class SVM:
    def __init__(self, kernel=linear_kernel, C=1.0):
        self.kernel = kernel
        self.C = C

    def fit(self, X, y, max_iter=1000, tol=1e-3):
        n_samples, n_features = X.shape
        self.alpha = np.zeros(n_samples)
        self.b = 0

        # 计算核矩阵
        K = np.zeros((n_samples, n_samples))
        for i in range(n_samples):
            for j in range(n_samples):
                K[i,j] = self.kernel(X[i], X[j])

        # 使用 SMO 算法求解
        for _ in range(max_iter):
            alpha_prev = np.copy(self.alpha)

            for i in range(n_samples):
                # 计算预测误差
                E_i = np.sum(self.alpha * y * K[:,i]) + self.b - y[i]

                # 选择第二个变量
                j = np.random.choice(list(range(i)) + list(range(i+1, n_samples)))
                E_j = np.sum(self.alpha * y * K[:,j]) + self.b - y[j]

                # 计算边界
                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
                eta = 2 * K[i,j] - K[i,i] - K[j,j]
                if eta >= 0:
                    continue

                # 更新 alpha_j
                alpha_j = self.alpha[j] - y[j] * (E_i - E_j) / eta
                alpha_j = max(L, min(H, alpha_j))

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

                # 更新 b
                b1 = self.b - E_i - y[i]*(alpha_i - self.alpha[i])*K[i,i] - y[j]*(alpha_j - self.alpha[j])*K[i,j]
                b2 = self.b - E_j - y[i]*(alpha_i - self.alpha[i])*K[i,j] - y[j]*(alpha_j - self.alpha[j])*K[j,j]

                if 0 < alpha_i < self.C:
                    self.b = b1
                elif 0 < alpha_j < self.C:
                    self.b = b2
                else:
                    self.b = (b1 + b2)/2

                self.alpha[i] = alpha_i
                self.alpha[j] = alpha_j

            # 检查收敛
            if np.linalg.norm(self.alpha - alpha_prev) < tol:
                break

        # 获取支持向量
        idx = self.alpha > 1e-5
        self.support_vectors = X[idx]
        self.support_vector_labels = y[idx]
        self.support_vector_alphas = self.alpha[idx]

        # 计算 w (仅对线性核有效)
        if self.kernel == linear_kernel:
            self.w = np.sum((self.alpha * y).reshape(-1,1) * X, axis=0)
        else:
            self.w = None

    def predict(self, X):
        if self.w is not None:
            return np.sign(np.dot(X, self.w) + self.b)
        else:
            y_pred = np.zeros(len(X))
            for i in range(len(X)):
                s = 0
                for alpha, sv_y, sv in zip(self.support_vector_alphas, 
                                          self.support_vector_labels, 
                                          self.support_vectors):
                    s += alpha * sv_y * self.kernel(X[i], sv)
                y_pred[i] = s
            return np.sign(y_pred + self.b)

# 训练模型
svm = SVM(kernel=linear_kernel, C=1.0)
svm.fit(X, y)

# 可视化
def plot_decision_boundary(model, X, y):
    h = 0.02
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))

    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)

    plt.contourf(xx, yy, Z, alpha=0.8)
    plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k')
    plt.scatter(model.support_vectors[:, 0], model.support_vectors[:, 1], 
                s=100, facecolors='none', edgecolors='k')
    plt.title('SVM Decision Boundary')
    plt.show()

plot_decision_boundary(svm, X, y)

3.2 使用 scikit-learn 实现

from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV

# 使用线性核
svm_linear = SVC(kernel='linear', C=1.0)
svm_linear.fit(X, y)

# 使用 RBF 核
svm_rbf = SVC(kernel='rbf', gamma=0.1, C=1.0)
svm_rbf.fit(X, y)

# 超参数调优
param_grid = {'C': [0.1, 1, 10, 100], 'gamma': [0.01, 0.1, 1, 10]}
grid = GridSearchCV(SVC(kernel='rbf'), param_grid, cv=5)
grid.fit(X, y)

print("Best parameters:", grid.best_params_)
print("Best score:", grid.best_score_)

4. 实践建议

  1. 超参数调优技巧:
  2. 使用网格搜索或随机搜索进行参数优化
  3. 对 C 参数尝试对数空间搜索(如 0.01, 0.1, 1, 10, 100)
  4. 对 RBF 核的 gamma 参数,可以从数据特征标准差的倒数附近开始尝试

  5. 大规模数据优化:

  6. 使用随机梯度下降的线性 SVM 实现(如 SGDClassifier)
  7. 考虑近似算法如 Core Vector Machine
  8. 使用子采样或在线学习策略

  9. 收敛问题解决:

  10. 检查数据是否已经标准化
  11. 调整容差 (tolerance) 参数
  12. 增加最大迭代次数
  13. 尝试不同的核函数或参数

5. 思考题

  1. 如何证明对偶问题的解等价于原始问题最优解?
  2. 核函数选择对求解效率有什么影响?
  3. 在线学习场景下,SVM 可以采取哪些优化策略?

通过本文的推导和实现,相信读者已经对 SVM 的对偶问题有了深入理解。在实际应用中,可以根据数据规模和特征选择合适的实现方式,并通过调参获得更好的性能。

正文完
 0
评论(没有评论)