一、项目背景详细介绍

支持向量机(SVM)是监督学习中经典且强大的算法,常用于二分类问题。SVM 的本质是求解一个凸二次规划(Quadratic Programming, QP)问题,得到对偶变量(拉格朗日乘子) αi\alpha_iαi​,并由此得到最优超平面。直接用通用二次规划求解器可以得到解,但代价高、实现复杂。

1998 年,John C. Platt 提出 Sequential Minimal Optimization(SMO)算法,用于高效地对 SVM 的对偶问题进行逐步求解。SMO 将大规模二次规划问题分解为一系列可解析(closed-form)求解的 2 变量子问题,从而避免复杂的数值求解器,提高训练速度并便于实现。

SMO 的核心思想:

  • 每次选取两个 α\alphaα 变量进行优化(保持其它变量不变),可以解析求解这两个变量的最优值;

  • 使用启发式策略选择两个变量,以加速收敛;

  • 迭代直到所有样本满足 KKT 条件或达到停止条件。

SMO 之所以重要:

  • 在实际机器学习系统中,SMO 是经典且实用的 SVM 训练方法;

  • 它直接体现对偶问题的结构化利用,便于学习优化、凸优化与核方法;

  • 对于中小规模数据集,SMO 简洁、可解释,便于教学。

本文将从数学推导、算法流程,到 C++ 实现逐步展开,并提供可直接运行的示例代码与说明。


二、项目需求详细介绍


三、相关技术详细介绍


四、实现思路详细介绍


五、完整实现代码

/***************************************************************
 * 文件名: smo_svm.cpp
 * 功能: C++ 教学版 SMO 实现(支持线性与 RBF 核)
 * 说明: 单文件可编译演示。不依赖外部 ML 库。
 * 编译: g++ -std=c++11 smo_svm.cpp -O2 -o smo_svm
 ***************************************************************/

#include <iostream>
#include <vector>
#include <cmath>
#include <cassert>
#include <string>
#include <fstream>
#include <sstream>
#include <limits>
#include <random>
#include <iomanip>

using std::vector;
using std::cout;
using std::endl;
using std::string;

/* ------------------ 核函数相关 ------------------ */
enum KernelType { LINEAR, RBF };

/* 计算高斯 RBF 核 */
double rbf_kernel(const vector<double>& x1, const vector<double>& x2, double gamma) {
    assert(x1.size() == x2.size());
    double sum = 0.0;
    for (size_t i = 0; i < x1.size(); ++i) {
        double d = x1[i] - x2[i];
        sum += d * d;
    }
    return std::exp(-gamma * sum);
}

/* 线性核 */
double linear_kernel(const vector<double>& x1, const vector<double>& x2) {
    assert(x1.size() == x2.size());
    double sum = 0.0;
    for (size_t i = 0; i < x1.size(); ++i) sum += x1[i] * x2[i];
    return sum;
}

/* ------------------ 简单 CSV 数据读取(输入特征+标签) ------------------ */
/* 假设 CSV 格式:x1,x2,...,xn,label  label ∈ {1,-1} */
bool loadCSV(const string& filename, vector<vector<double>>& X, vector<int>& y) {
    std::ifstream in(filename);
    if (!in.is_open()) {
        std::cerr << "无法打开数据文件: " << filename << std::endl;
        return false;
    }
    string line;
    while (std::getline(in, line)) {
        if (line.empty()) continue;
        std::stringstream ss(line);
        string token;
        vector<double> row;
        while (std::getline(ss, token, ',')) {
            row.push_back(std::stod(token));
        }
        if (row.size() < 2) continue;
        int label = static_cast<int>(row.back());
        row.pop_back();
        X.push_back(row);
        y.push_back(label);
    }
    in.close();
    return true;
}

/* ------------------ SVM 模型类(SMO训练) ------------------ */
class SVM {
public:
    SVM(double C = 1.0, double tol = 1e-3, double eps = 1e-3, KernelType ktype = RBF, double gamma = 0.5)
        : C(C), tol(tol), eps(eps), kernelType(ktype), gamma(gamma), b(0.0) {}

    // 训练主入口
    void train(const vector<vector<double>>& X_train, const vector<int>& y_train, int max_passes = 10) {
        // 初始化数据
        X = X_train;
        y = y_train;
        N = static_cast<int>(X.size());
        assert(N == static_cast<int>(y.size()));
        if (N == 0) return;
        dim = static_cast<int>(X[0].size());
        alpha.assign(N, 0.0);
        E.assign(N, 0.0);
        b = 0.0;

        // 预计算核矩阵(教学版:可注释改为按需计算以节省内存)
        K.assign(N, vector<double>(N, 0.0));
        for (int i = 0; i < N; ++i) {
            for (int j = 0; j < N; ++j) {
                if (kernelType == LINEAR) K[i][j] = linear_kernel(X[i], X[j]);
                else K[i][j] = rbf_kernel(X[i], X[j], gamma);
            }
        }

        // 初始化误差缓存 E_i = f(x_i) - y_i (f初始为0)
        for (int i = 0; i < N; ++i) E[i] = - (double)y[i];

        // SMO 主循环
        int passes = 0;
        int num_changed = 0;
        while (passes < max_passes) {
            num_changed = 0;
            for (int i = 0; i < N; ++i) {
                num_changed += examineExample(i);
            }
            if (num_changed == 0) passes++;
            else passes = 0;
            // 可选:打印进度
            // cout << "passes=" << passes << " num_changed=" << num_changed << endl;
        }
        // 训练完成,输出支持向量统计
        int sv = 0;
        for (int i = 0; i < N; ++i) if (alpha[i] > 0) sv++;
        cout << "训练完成. 支持向量数: " << sv << " / " << N << endl;
    }

    // 预测单个样本分数 f(x)
    double decisionFunction(const vector<double>& x) const {
        double sum = -b; // we'll add sum(alpha_i*y_i*K) and then +b; choose consistent sign
        for (int i = 0; i < N; ++i) {
            if (alpha[i] > 0) {
                double k;
                if (kernelType == LINEAR) k = linear_kernel(X[i], x);
                else k = rbf_kernel(X[i], x, gamma);
                sum += alpha[i] * y[i] * k;
            }
        }
        // return sum + b; // depending on sign convention
        return sum; // we've subtracted b initially; consistent with updates below
    }

    // 预测类标签
    int predict(const vector<double>& x) const {
        double val = decisionFunction(x);
        return (val >= 0) ? 1 : -1;
    }

    // 保存模型(简单文本格式)
    bool saveModel(const string& path) const {
        std::ofstream out(path);
        if (!out.is_open()) return false;
        out << "C " << C << "\n";
        out << "b " << b << "\n";
        out << "kernel " << (kernelType == LINEAR ? "linear" : "rbf") << " gamma " << gamma << "\n";
        out << "N " << N << " dim " << dim << "\n";
        for (int i = 0; i < N; ++i) {
            if (alpha[i] > 0) {
                out << alpha[i] << " " << y[i];
                for (int d = 0; d < dim; ++d) out << " " << X[i][d];
                out << "\n";
            }
        }
        out.close();
        return true;
    }

    // 载入模型(简化:只支持上述 save 格式)
    bool loadModel(const string& path) {
        std::ifstream in(path);
        if (!in.is_open()) return false;
        string line;
        vector<vector<double>> newX;
        vector<int> newY;
        vector<double> newAlpha;
        double Ctmp=1.0; double btmp=0.0; string kname; double gammatmp=0.5;
        int Ntmp=0, dimtmp=0;
        while (std::getline(in, line)) {
            if (line.empty()) continue;
            std::stringstream ss(line);
            string tag;
            ss >> tag;
            if (tag == "C") ss >> Ctmp;
            else if (tag == "b") ss >> btmp;
            else if (tag == "kernel") { ss >> kname >> tag >> gammatmp; }
            else if (tag == "N") { ss >> Ntmp >> tag >> dimtmp; }
            else {
                double a; int lab;
                ss.clear();
                ss.str(line);
                ss >> a >> lab;
                vector<double> row;
                double v;
                while (ss >> v) row.push_back(v);
                newAlpha.push_back(a);
                newY.push_back(lab);
                newX.push_back(row);
            }
        }
        in.close();
        // set model
        C = Ctmp; b = btmp; gamma = gammatmp;
        kernelType = (kname == "linear") ? LINEAR : RBF;
        X = newX; y = newY; N = (int)X.size(); dim = (N>0)? (int)X[0].size():0;
        alpha.assign(N, 0.0);
        for (int i = 0; i < (int)newAlpha.size(); ++i) alpha[i] = newAlpha[i];
        // precompute K
        K.assign(N, vector<double>(N, 0.0));
        for (int i = 0; i < N; ++i)
            for (int j = 0; j < N; ++j)
                K[i][j] = (kernelType==LINEAR? linear_kernel(X[i], X[j]) : rbf_kernel(X[i],X[j],gamma));
        // calc E
        E.assign(N, 0.0);
        for (int i = 0; i < N; ++i) {
            double sum=0;
            for (int j=0;j<N;++j) sum += alpha[j]*y[j]*K[j][i];
            E[i] = sum - y[i];
        }
        return true;
    }

private:
    // 数据与参数
    vector<vector<double>> X; // 训练特征
    vector<int> y;            // 标签 (+1/-1)
    int N = 0;                // 样本数
    int dim = 0;              // 特征维
    vector<double> alpha;     // 拉格朗日乘子
    vector<double> E;         // 误差缓存 E_i = f(x_i) - y_i
    double b;                 // 偏置
    vector<vector<double>> K; // 核矩阵(预计算)

    // 超参数
    double C;                 // 惩罚系数
    double tol;               // 容忍度, 检查 KKT
    double eps;               // 数值稳定性阈值,用于判断 alpha 变化是否显著
    KernelType kernelType;
    double gamma;             // RBF gamma

    // 选择第二个 alpha 的启发式:选择使 |E_i - E_j| 最大的 j
    int selectJ(int i, double Ei) {
        int bestJ = -1;
        double maxDelta = 0.0;
        for (int j = 0; j < N; ++j) {
            if (j == i) continue;
            double Ej = E[j];
            double delta = std::abs(Ei - Ej);
            if (delta > maxDelta) {
                maxDelta = delta;
                bestJ = j;
            }
        }
        if (bestJ != -1) return bestJ;
        // 若没找到(例如初期),随机选择
        std::mt19937 gen((unsigned)time(nullptr));
        std::uniform_int_distribution<int> dist(0, N-1);
        int j = dist(gen);
        while (j == i) j = dist(gen);
        return j;
    }

    // 检查样本 i 是否满足 KKT 条件(容忍 tol)
    bool violatesKKT(int i) {
        double yi = y[i];
        double ai = alpha[i];
        double gi = 0.0;
        for (int j = 0; j < N; ++j) gi += alpha[j] * y[j] * K[j][i];
        gi += b;
        double ri = yi * gi;
        if ((ai < C && ri < 1 - tol) || (ai > 0 && ri > 1 + tol)) return true;
        return false;
    }

    // 主动检查并尝试优化第 i 个样本
    int examineExample(int i2) {
        double y2 = y[i2];
        double alpha2 = alpha[i2];
        double E2 = E[i2];
        double r2 = E2 * y2;
        // 检查是否违反KKT
        if ((r2 < -tol && alpha2 < C) || (r2 > tol && alpha2 > 0)) {
            // 1. 优先选择最大的 |E1 - E2| 的 j
            int j = selectJ(i2, E2);
            if (j >= 0) {
                if (takeStep(i2, j)) return 1;
            }
            // 2. 随机尝试其他非边界点(0 < alpha < C)
            for (int jj = 0; jj < N; ++jj) {
                int i = (jj + i2 + 1) % N;
                if (alpha[i] > 0 && alpha[i] < C) {
                    if (takeStep(i2, i)) return 1;
                }
            }
            // 3. 随机尝试所有点
            for (int jj = 0; jj < N; ++jj) {
                int i = (jj + i2 + 1) % N;
                if (takeStep(i2, i)) return 1;
            }
        }
        return 0;
    }

    // 对 i1,i2 两个样本进行优化(Platt's takeStep)
    bool takeStep(int i1, int i2) {
        if (i1 == i2) return false;
        double alpha1 = alpha[i1]; double alpha2 = alpha[i2];
        int y1 = y[i1]; int y2 = y[i2];
        double E1 = E[i1]; double E2 = E[i2];
        double s = y1 * y2;

        // 计算 L 与 H
        double L, H;
        if (y1 != y2) {
            L = std::max(0.0, alpha2 - alpha1);
            H = std::min(C, C + alpha2 - alpha1);
        } else {
            L = std::max(0.0, alpha2 + alpha1 - C);
            H = std::min(C, alpha2 + alpha1);
        }
        if (L == H) return false;

        // 计算 eta
        double k11 = K[i1][i1];
        double k12 = K[i1][i2];
        double k22 = K[i2][i2];
        double eta = k11 + k22 - 2 * k12;
        double a2_new;
        if (eta > 0) {
            a2_new = alpha2 + y2 * (E1 - E2) / eta;
            // 裁剪
            if (a2_new < L) a2_new = L;
            else if (a2_new > H) a2_new = H;
        } else {
            // eta <= 0 的情况:目标函数近似不凸
            // 使用双端比较(把目标函数在端点处的值进行比较)
            // 计算目标函数值以选择端点(教学实现)
            auto f = [&](double a2) {
                double a1 = alpha1 + s * (alpha2 - a2);
                double term = a1 + a2;
                // 目标: W(a1,a2) = ...
                double res = a1 + a2 - 0.5 * k11 * a1 * a1 - 0.5 * k22 * a2 * a2 - s * k12 * a1 * a2;
                return res;
            };
            double fL = f(L);
            double fH = f(H);
            if (fL > fH + 1e-12) a2_new = L;
            else if (fH > fL + 1e-12) a2_new = H;
            else a2_new = alpha2;
        }

        if (std::abs(a2_new - alpha2) < eps * (a2_new + alpha2 + eps)) return false;

        double a1_new = alpha1 + s * (alpha2 - a2_new);

        // 更新阈值 b
        double b1 = b - E1 - y1 * (a1_new - alpha1) * k11 - y2 * (a2_new - alpha2) * k12;
        double b2 = b - E2 - y1 * (a1_new - alpha1) * k12 - y2 * (a2_new - alpha2) * k22;
        double b_new;
        if (0 < a1_new && a1_new < C) b_new = b1;
        else if (0 < a2_new && a2_new < C) b_new = b2;
        else b_new = 0.5 * (b1 + b2);

        // 提交更新
        alpha[i1] = a1_new;
        alpha[i2] = a2_new;
        b = b_new;

        // 更新误差缓存 E[k] = f(x_k) - y[k]
        for (int k = 0; k < N; ++k) {
            E[k] += y1 * (a1_new - alpha1) * K[i1][k] + y2 * (a2_new - alpha2) * K[i2][k] + (b - b_new);
        }
        // 强制 i1,i2 的误差重置
        E[i1] = 0.0;
        E[i2] = 0.0;

        return true;
    }

}; // end class SVM

/* ------------------ 示例使用 main ------------------ */

int main(int argc, char** argv) {
    cout << "SMO SVM 教学实现 (C++)" << endl;
    // 简单示例:生成随机线性可分数据或载入 CSV
    vector<vector<double>> X;
    vector<int> y;

    if (argc >= 2) {
        // 从 CSV 读取
        string path = argv[1];
        if (!loadCSV(path, X, y)) return -1;
    } else {
        // 生成合成数据(两个高斯类)
        std::mt19937 gen(42);
        std::normal_distribution<double> d1(2.0, 0.8);
        std::normal_distribution<double> d2(-2.0, 0.8);
        for (int i = 0; i < 50; ++i) {
            X.push_back({d1(gen), d1(gen)});
            y.push_back(1);
            X.push_back({d2(gen), d2(gen)});
            y.push_back(-1);
        }
    }

    // 创建 SVM(C=1.0, tol=1e-3, eps=1e-3, RBF gamma=0.5)
    SVM svm(1.0, 1e-3, 1e-3, RBF, 0.5);
    svm.train(X, y, 20);

    // 在训练集上预测并输出精度
    int correct = 0;
    for (size_t i = 0; i < X.size(); ++i) {
        int pred = svm.predict(X[i]);
        if (pred == y[i]) correct++;
    }
    cout << "训练集精度: " << (double)correct / X.size() << endl;

    // 保存模型示例
    svm.saveModel("smo_model.txt");
    cout << "模型已保存到 smo_model.txt" << endl;

    return 0;
}

六、代码详细解读

  1. 核函数 rbf_kernel / linear_kernel

    • 作用:分别计算 RBF(高斯)核与线性内积核值,用于构建核矩阵 K 或在线计算 K(x_i,x)。

  2. loadCSV

    • 作用:从简易 CSV 文件加载训练数据,假设每行格式为 x1,x2,...,xn,label

  3. SVM 类构造与成员

    • 存储训练数据 X、标签 y、α、误差缓存 E、偏置 b、核矩阵 K 等。支持构造时指定超参数 C、tol、eps、核类型与 γ。

  4. train()

    • 作用:SMO 主入口。初始化 α、E、核矩阵,执行 Platt SMO 主循环(passes 控制未改变次数终止),并输出支持向量数量。

  5. selectJ / examineExample

    • 作用:选择第二变量 j 的启发式(寻找使 |E_i - E_j| 最大的 j),并按 Platt 的策略尝试不同候选 j:

      • 先尝试最大误差差值的 j;

      • 再尝试非边界点(0 < α < C);

      • 最后尝试所有点。

  6. takeStep(i1,i2)

    • 作用:对所选两变量执行具体的解析优化步骤:

      • 计算 L 与 H(α 的可行区间);

      • 计算 η 并求解 α2 的新值(若 η > 0),或对端点值比较选择;

      • 判断更新幅度是否足够显著(小于 eps 则不更新);

      • 更新 α1、α2、b,并增量更新误差缓存 E。

  7. decisionFunction / predict

    • 作用:预测函数与类别输出。decisionFunction 计算 f(x)=∑αiyiK(xi,x)+bf(x) = \sum \alpha_i y_i K(x_i,x) + bf(x)=∑αi​yi​K(xi​,x)+b;predict 返回 sign。

  8. saveModel / loadModel

    • 作用:将训练好的模型(支持向量 α、标签、特征与参数)保存/加载到文本文件,便于后续使用。

  9. main 示例

    • 作用:示例数据准备(从 CSV 或合成数据),训练 SVM,评估训练集精度并保存模型。


七、项目详细总结

本文实现了 基于 Platt SMO 的二分类 SVM(教学版):

  • 支持线性与 RBF 核;

  • 实现了 SMO 的主循环、变量选择启发式、两个变量的解析更新与误差缓存;

  • 提供训练、预测、模型保存与加载功能;

  • 代码自包含、注释详尽,便于教学与实验。

注意事项与局限:

  • 本实现为教学版,预计算了完整核矩阵 K,适用于中小规模数据集(几千样本以内);若数据量大,需改成按需计算或使用核近似方法(如近似特征、核缓存)。

  • 对于 η <= 0 的情形,采用端点比较法,这是常见处理;工程化实现可采用更多数值稳定技巧与启发式改进。

  • 性能提升方向包括:核缓存优化、多线程并行、启发式变量选取改进(如序列启发式)、使用更高效的数据存储。


八、项目常见问题及解答

Q1:SMO 为什么每次只能优化两个 α?
A:因为带有线性约束 ∑αiyi=0\sum \alpha_i y_i = 0∑αi​yi​=0,优化两个变量即可保持约束且能解析求解,从而避免复杂 QP 求解器。

Q2:训练速度慢怎么办?
A:可考虑:

  • 使用核缓存或按需计算核;

  • 使用更好的启发式(例如最大步长、启发式启发);

  • 数据缩放、降维或使用线性 SVM(liblinear);

  • 并行化或使用近似方法(核近似)。

Q3:为什么需要 tol 与 eps?
A:tol 用于 KKT 检查的容忍度,防止无限追求数值精度;eps 用于判断 α 的实质性变化,避免数值噪声导致频繁小更新。

Q4:如何选择 gamma(RBF)与 C?
A:常通过交叉验证选择;一般经验:

  • gamma 大:模型更复杂,可能过拟合;

  • C 大:对误分类惩罚大,硬间隔倾向。

Q5:如何扩展到多类分类?
A:常用一对一(one-vs-one)或一对多(one-vs-rest)策略,训练多个二分类 SVM 并组合预测。


九、扩展方向与性能优化

  1. 核矩阵缓存优化:当前实现预计算并存储完整 K 矩阵,若内存受限可实现核缓存(LRU 缓存)或按需计算。

  2. 启发式改进:实现更先进的变量选择策略(如最大误差、启发式交替扫描)以提速。

  3. SMO 数值稳定性:对 η 接近 0 的情况采用更稳健的处理方式(比如二次函数精确求解与步长限制)。

  4. 多线程并行:某些步骤(如误差缓存重计算)可并行化;训练主循环可用锁分区策略并行变体。

  5. 内存与 I/O 优化:将模型持久化为二进制格式以节省加载时间;支持稀疏特征(常见于文本数据)。

  6. 集成交叉验证:自动搜索 C 与 γ 的最佳组合,集成网格搜索或贝叶斯优化。

  7. 图形界面可视化:对于二维数据,绘制决策边界、支持向量与误差曲面,便于教学演示。

  8. 对接生产库:可参考 libsvm/liblinear 的工程实现,学习更多实战优化细节。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐