C++实现支持向量机(SVM)从入门到实战:原理详解与代码实现

1次阅读
没有评论

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

image.webp

背景介绍

支持向量机 (SVM) 是机器学习中经典的监督学习算法,特别适合解决中小规模数据集的分类问题。它的核心思想是在特征空间中找到一个最优超平面,使得不同类别的数据点能够被最大间隔分开。SVM 在文本分类、图像识别、生物信息学等领域都有广泛应用。

C++ 实现支持向量机 (SVM) 从入门到实战:原理详解与代码实现

相比 Python 生态中成熟的 scikit-learn 库,用 C ++ 实现 SVM 可以更好地理解算法细节,并且在性能敏感场景中获得更高效的计算。本文将带您从零开始,用现代 C ++ 实现一个完整的 SVM 分类器。

数学原理

最大间隔分类器

SVM 的核心是寻找一个超平面 wx + b = 0,使得两类样本到这个超平面的最小距离 (即间隔) 最大化。这个优化问题可以表示为:

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

支持向量

在解这个优化问题时,只有少数靠近决策边界的样本点 (称为支持向量) 会影响最终结果。这些支持向量满足 y_i(wx_i + b) = 1。

核函数技巧

对于非线性可分问题,SVM 通过核函数将数据映射到高维空间,使其在高维空间中线性可分。常用的核函数包括:

  • 线性核:$K(x_i, x_j) = x_i^T x_j$
  • RBF 核:$K(x_i, x_j) = \exp(-\gamma ||x_i – x_j||^2)$

C++ 实现

数据结构设计

我们使用 Eigen 库来处理矩阵运算,这是现代 C ++ 中高效的线性代数库。

#include <Eigen/Dense>
#include <vector>

struct SVM {
    Eigen::VectorXd alphas;  // 拉格朗日乘子
    Eigen::VectorXd w;       // 权重向量(仅线性核)
    double b = 0;            // 偏置项
    std::vector<Eigen::VectorXd> support_vectors; // 支持向量
    std::vector<double> support_vector_labels;    // 支持向量标签
    std::vector<double> support_vector_alphas;    // 支持向量对应的 alpha
};

SMO 算法实现

序列最小优化 (SMO) 是求解 SVM 对偶问题的高效算法。其核心思想是每次选择两个变量进行优化,固定其他变量。

void SMO(const Eigen::MatrixXd& X, const Eigen::VectorXd& y, SVM& model, 
         double C = 1.0, double tol = 1e-3, int max_passes = 5) {
    // 初始化
    int m = X.rows();
    Eigen::VectorXd alphas = Eigen::VectorXd::Zero(m);
    double b = 0;

    int passes = 0;
    while (passes < max_passes) {
        // 实现 SMO 的主循环
        // ...
    }

    // 保存模型参数
    model.alphas = alphas;
    model.b = b;
    // 提取支持向量...
}

完整代码示例

下面是一个简化但完整的 SVM 实现框架:

#include <iostream>
#include <Eigen/Dense>
#include <cmath>
#include <vector>

// 核函数接口
class Kernel {
public:
    virtual double compute(const Eigen::VectorXd& x1, const Eigen::VectorXd& x2) const = 0;
};

// 线性核
class LinearKernel : public Kernel {
public:
    double compute(const Eigen::VectorXd& x1, const Eigen::VectorXd& x2) const override {return x1.dot(x2);
    }
};

// RBF 核
class RBFKernel : public Kernel {
public:
    RBFKernel(double gamma) : gamma(gamma) {}

    double compute(const Eigen::VectorXd& x1, const Eigen::VectorXd& x2) const override {return exp(-gamma * (x1 - x2).squaredNorm());
    }

private:
    double gamma;
};

// SVM 类实现
class SVM {
public:
    void train(const Eigen::MatrixXd& X, const Eigen::VectorXd& y, const Kernel* kernel, 
               double C = 1.0, double tol = 1e-3, int max_passes = 5);

    double predict(const Eigen::VectorXd& x);

private:
    // 成员变量和方法实现...
};

// 训练函数实现
void SVM::train(const Eigen::MatrixXd& X, const Eigen::VectorXd& y, const Kernel* kernel, 
                double C, double tol, int max_passes) {
    // SMO 算法实现
    // ...
}

// 预测函数
double SVM::predict(const Eigen::VectorXd& x) {
    // 使用支持向量进行预测
    // ...
    return result;
}

int main() {
    // 示例数据
    Eigen::MatrixXd X(4, 2);
    X << 1, 1,
         2, 2,
         1, 2,
         2, 1;
    Eigen::VectorXd y(4);
    y << 1, 1, -1, -1;

    // 训练 SVM
    RBFKernel rbf_kernel(0.1);
    SVM svm;
    svm.train(X, y, &rbf_kernel);

    // 测试预测
    Eigen::VectorXd test(2);
    test << 1.5, 1.5;
    std::cout << "Prediction:" << svm.predict(test) << std::endl;

    return 0;
}

性能考量

时间复杂度

  • 训练阶段:SMO 算法的时间复杂度一般为 O(n^2)到 O(n^3),其中 n 是样本数量
  • 预测阶段:O(m·d),其中 m 是支持向量数量,d 是特征维度

内存优化

  1. 对于大规模数据,可以只缓存核矩阵的一部分
  2. 使用稀疏矩阵存储稀疏数据
  3. 实现核缓存机制,避免重复计算

避坑指南

  1. 特征缩放问题:SVM 对特征的尺度敏感,务必对特征进行标准化
  2. 核参数选择:RBF 核的 γ 参数需要仔细调优,太大容易过拟合,太小会欠拟合
  3. 收敛问题 :SMO 算法可能不收敛,需要设置合理的容差(tol) 和最大迭代次数
  4. 数值稳定性:注意浮点数比较和计算中的数值稳定性问题

进阶方向

  1. 多分类 SVM:通过一对多 (One-vs-Rest) 或一对一 (One-vs-One) 策略实现
  2. 在线学习 SVM:增量式更新模型,适用于流式数据
  3. 结构化 SVM:扩展 SVM 处理结构化输出问题

思考题

  1. 如何修改我们的实现,使其支持稀疏矩阵运算以提高内存效率?
  2. 除了线性核和 RBF 核,还有哪些常用核函数?它们各自适用于什么场景?
  3. 当面对超大规模数据集时,有哪些策略可以加速 SVM 的训练过程?

总结

通过本文,我们实现了从 SVM 原理到 C ++ 代码的完整流程。虽然现代机器学习框架提供了现成的 SVM 实现,但自己动手实现一遍能够加深对算法本质的理解。在实际应用中,还需要考虑交叉验证调参、模型评估等环节。希望这篇文章能为您的机器学习之旅提供帮助!

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