C++实现smo算法(附带源码)
一、项目背景详细介绍
支持向量机(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;
}
六、代码详细解读
-
核函数 rbf_kernel / linear_kernel
-
作用:分别计算 RBF(高斯)核与线性内积核值,用于构建核矩阵 K 或在线计算 K(x_i,x)。
-
-
loadCSV
-
作用:从简易 CSV 文件加载训练数据,假设每行格式为
x1,x2,...,xn,label。
-
-
SVM 类构造与成员
-
存储训练数据 X、标签 y、α、误差缓存 E、偏置 b、核矩阵 K 等。支持构造时指定超参数 C、tol、eps、核类型与 γ。
-
-
train()
-
作用:SMO 主入口。初始化 α、E、核矩阵,执行 Platt SMO 主循环(passes 控制未改变次数终止),并输出支持向量数量。
-
-
selectJ / examineExample
-
作用:选择第二变量 j 的启发式(寻找使 |E_i - E_j| 最大的 j),并按 Platt 的策略尝试不同候选 j:
-
先尝试最大误差差值的 j;
-
再尝试非边界点(0 < α < C);
-
最后尝试所有点。
-
-
-
takeStep(i1,i2)
-
作用:对所选两变量执行具体的解析优化步骤:
-
计算 L 与 H(α 的可行区间);
-
计算 η 并求解 α2 的新值(若 η > 0),或对端点值比较选择;
-
判断更新幅度是否足够显著(小于 eps 则不更新);
-
更新 α1、α2、b,并增量更新误差缓存 E。
-
-
-
decisionFunction / predict
-
作用:预测函数与类别输出。decisionFunction 计算 f(x)=∑αiyiK(xi,x)+bf(x) = \sum \alpha_i y_i K(x_i,x) + bf(x)=∑αiyiK(xi,x)+b;predict 返回 sign。
-
-
saveModel / loadModel
-
作用:将训练好的模型(支持向量 α、标签、特征与参数)保存/加载到文本文件,便于后续使用。
-
-
main 示例
-
作用:示例数据准备(从 CSV 或合成数据),训练 SVM,评估训练集精度并保存模型。
-
七、项目详细总结
本文实现了 基于 Platt SMO 的二分类 SVM(教学版):
-
支持线性与 RBF 核;
-
实现了 SMO 的主循环、变量选择启发式、两个变量的解析更新与误差缓存;
-
提供训练、预测、模型保存与加载功能;
-
代码自包含、注释详尽,便于教学与实验。
注意事项与局限:
-
本实现为教学版,预计算了完整核矩阵 K,适用于中小规模数据集(几千样本以内);若数据量大,需改成按需计算或使用核近似方法(如近似特征、核缓存)。
-
对于 η <= 0 的情形,采用端点比较法,这是常见处理;工程化实现可采用更多数值稳定技巧与启发式改进。
-
性能提升方向包括:核缓存优化、多线程并行、启发式变量选取改进(如序列启发式)、使用更高效的数据存储。
八、项目常见问题及解答
Q1:SMO 为什么每次只能优化两个 α?
A:因为带有线性约束 ∑αiyi=0\sum \alpha_i y_i = 0∑αiyi=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 并组合预测。
九、扩展方向与性能优化
-
核矩阵缓存优化:当前实现预计算并存储完整 K 矩阵,若内存受限可实现核缓存(LRU 缓存)或按需计算。
-
启发式改进:实现更先进的变量选择策略(如最大误差、启发式交替扫描)以提速。
-
SMO 数值稳定性:对 η 接近 0 的情况采用更稳健的处理方式(比如二次函数精确求解与步长限制)。
-
多线程并行:某些步骤(如误差缓存重计算)可并行化;训练主循环可用锁分区策略并行变体。
-
内存与 I/O 优化:将模型持久化为二进制格式以节省加载时间;支持稀疏特征(常见于文本数据)。
-
集成交叉验证:自动搜索 C 与 γ 的最佳组合,集成网格搜索或贝叶斯优化。
-
图形界面可视化:对于二维数据,绘制决策边界、支持向量与误差曲面,便于教学演示。
-
对接生产库:可参考 libsvm/liblinear 的工程实现,学习更多实战优化细节。
更多推荐
所有评论(0)