SVM使用c++实现

下面是使用c++实现一个简单的支持向量机(SVM)分类器,并提供了从文件中读取数据、训练模型和对测试数据进行预测的功能。

SVM 实现是基于线性核函数的,并采用了简化的序列最小优化(SMO)算法进行训练

SVM推导

SVM 旨在找到一个超平面,使得正负样本间的间隔最大化。

对于线性可分的数据集,SVM 的目标函数和约束条件可以写为:

  1. 目标函数(最小化):

    1. 12w2\frac{1}{2} \| \mathbf{w} \|^2

      其中 ww 是超平面的法向量。

  2. 约束条件

    1. yi(wxi+b)1,  iy_i (\mathbf{w} \cdot \mathbf{x}_i + b) \geq 1, \; \forall i

    其中 yiy_i是第i个样本的类标签,xix_i是第i个样本的特征向量,bb是偏置项。

通过拉格朗日乘子法引入拉格朗日乘子 αi0\alpha_i \geq 0,得到拉格朗日函数:

L(w,b,α)=12w2i=1mαi[yi(wxi+b)1]L(\mathbf{w}, b, \boldsymbol{\alpha}) = \frac{1}{2} \| \mathbf{w} \|^2 - \sum_{i=1}^m \alpha_i [y_i (\mathbf{w} \cdot \mathbf{x}_i + b) - 1]

SMO 算法

SMO 算法的目标是求解这个拉格朗日函数的对偶问题,即最大化 $L(\mathbf{w}, b, \boldsymbol{\alpha}) $关于 α\boldsymbol{\alpha} 的值,同时满足约束条件 αi0\alpha_i \geq 0i=1mαiyi=0\sum_{i=1}^m \alpha_i y_i = 0,关键是每次只优化两个拉格朗日乘子,因此固定其他乘子。

SMO 算法的主要步骤包括:

  1. 选择一对需要优化的拉格朗日乘子 alphaialpha_iαj\alpha_j

  2. 固定 αi\alpha_iαj\alpha_j 以外的所有乘子,只对这两个乘子进行优化。

  3. 计算新的 αi\alpha_iαj\alpha_j

  4. 更新偏置项 bb

  5. 重复这个过程,直到所有的拉格朗日乘子满足 KKT 条件。

KKT 条件

在 SVM 的背景下,KKT(Karush-Kuhn-Tucker)条件是确定是否找到最优解的关键。对于每个样本 ii,KKT 条件为:

  1. $ \alpha_i = 0 \Rightarrow y_i (\mathbf{w} \cdot \mathbf{x}_i + b) \geq 1 $
  2. $ \alpha_i > 0 \Rightarrow y_i (\mathbf{w} \cdot \mathbf{x}_i + b) = 1 $
  3. $ \alpha_i = C \Rightarrow y_i (\mathbf{w} \cdot \mathbf{x}_i + b) \leq 1 $

其中CC是正则化参数,在软间隔 SVM 中使用。

代码思路

SVM 类

SVM 类是代码的核心,实现了 SVM 的主要功能:

  1. 构造函数:接受惩罚参数 C、容忍度 tolerance 和最大迭代次数 max_passes 作为参数。
  2. fit 方法:用于训练 SVM。它接受输入特征 X 和类标签 y,然后使用 SMO 算法更新拉格朗日乘子(alphas)和偏置项(b)。
  3. predict 方法:根据训练后的模型参数对新数据进行预测。
  4. getWeights 方法:计算并返回权重向量,这是由 alphasyX 组合而成的。
  5. **kernel**计算线性核、f用于计算决策函数的值。
  6. fit 方法的具体流程
    1. initialize 方法负责设置初始状态。
    2. mainLoop 方法控制整个算法的迭代过程,直到达到 max_passes 或找到最优解。
    3. mainLoop 内部,iterateThroughDataSet 负责遍历所有数据点,并对每个数据点调用 examineExample
    4. examineExample 方法检查并选择是否需要优化特定的 alpha 值。它调用 selectJ 来选择一个合适的 j 索引,并调用 updateAlphasAndB 来更新 alpha 值和偏置项 b。
    5. updateAlphasAndB 方法进一步分解为更具体的操作,如计算 L 和 H 的边界(computeLH),计算 eta(computeEta),更新 alpha 值(updateAlphaJupdateAlphaI),以及更新偏置项 b(updateB)。

主函数 main

  1. 从文件读取训练数据:使用 utils::getData 读取 data.txt,得到训练特征 X 和类标签 y
  2. 训练 SVM:创建 SVM 实例,调用 fit 方法进行训练。
  3. 输出训练后的模型参数:打印权重和偏置项。
  4. 读取测试数据:从 test.txt 中读取测试数据(仅特征,无类标签)。
  5. 对测试数据进行预测并规范化结果:对每个测试样本进行预测,如果预测值大于 0.7 则归类为 1,小于 -0.7 则归类为 -1,否则保持原值。

SVM 训练过程

fit 方法中,SVM 使用 SMO 算法来找到最佳的拉格朗日乘子。SMO 算法通过迭代方式优化这些乘子,以最小化损失函数。每一步迭代中,算法选取一对乘子进行优化及计算和更新偏置项 b,以及处理乘子的上下界。

1. 构造函数

初始化 SVM 的正则化参数 $ C $、容忍度(tolerance)、和最大迭代次数(max_passes)。

1
2
SVM(double C, double tolerance, int max_passes)
: C(C), tolerance(tolerance), max_passes(max_passes) {}

2. 获取权重

对应步骤:

  • 计算权重向量 w\mathbf{w}
  • 公式: w=i=1mαiyixi\mathbf{w} = \sum_{i=1}^{m} \alpha_{i} y_{i} \mathbf{x}_{i}
1
2
3
4
5
6
7
8
9
vector<double> getWeights() const {
vector<double> weights(n, 0.0);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
weights[j] += alphas[i] * y[i] * X[i][j];
}
}
return weights;
}

3. 预测方法

对应步骤:

  • 使用训练好的模型进行预测。
  • 公式: f(x)=i=1mαiyiK(x,xi)+bf(\mathbf{x}) = \sum_{i=1}^{m} \alpha_{i} y_{i} K(\mathbf{x}, \mathbf{x}_{i}) + b
1
2
3
4
5
6
7
double predict(const vector<double>& x) {
double sum = 0.0;
for (int i = 0; i < m; i++) {
sum += alphas[i] * y[i] * kernel(x, X[i]);
}
return sum + b;
}

4. 初始化操作

  • 设置 SVM 的训练数据和参数。
1
2
3
4
5
6
7
8
void initialize(const vector<vector<double>>& X, const vector<double>& y) {
this->X = X;
this->y = y;
m = X.size();
n = X[0].size();
alphas.resize(m, 0.0);
b = 0.0;
}

5. 主循环

对应步骤:

  • SMO 算法的主循环,迭代优化 α 值。
1
2
3
4
5
6
7
8
void mainLoop() {
int passes = 0;
while (passes < max_passes) {
int num_changed_alphas = iterateThroughDataSet();
if (num_changed_alphas == 0) passes++;
else passes = 0;
}
}

6. 数据集迭

对应步骤:

  • 遍历整个数据集来优化 α 值。
1
2
3
4
5
6
7
int iterateThroughDataSet() {
int num_changed_alphas = 0;
for (int i = 0; i < m; i++) {
num_changed_alphas += examineExample(i);
}
return num_changed_alphas;
}

7. 检查单个样本

对应步骤:

  • 检查和选择 α 值是否需要优化。
  • 检查 KKT 条件。
1
2
3
4
5
6
7
8
9
10
int examineExample(int i) {
double Ei = f(X[i]) - y[i];
if ((y[i] * Ei < -tolerance && alphas[i] < C) || (y[i] * Ei > tolerance && alphas[i] > 0)) {
int j = selectJ(i, Ei);
if (j >= 0) {
return updateAlphasAndB(i, j);
}
}
return 0;
}

8. 计算 L 和 H 的边界

对应步骤:

  • 计算 α 值的剪辑边界。
1
2
3
4
5
6
7
8
9
10
// 计算边界 L 和 H
void computeLH(int i, int j, double& L, double& H) {
if (y[i] != y[j]) {
L = max(0.0, alphas[j] - alphas[i]);
H = min(C, C + alphas[j] - alphas[i]);
} else {
L = max(0.0, alphas[i] + alphas[j] - C);
H = min(C, alphas[i] + alphas[j]);
}
}

9. 计算 eta

对应步骤:

  • 计算 α 值优化时的步长。
1
2
3
4
// 计算 eta
double computeEta(int i, int j) {
return 2.0 * kernel(X[i], X[j]) - kernel(X[i], X[i]) - kernel(X[j], X[j]);
}

10. 更新 alphaialpha_ialphajalpha_j

对应步骤:

  • 更新 α 值和偏置项 b。
  • 根据优化问题的二次规划来更新 α 值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 更新 alpha_i 和 alpha_j
int updateAlphasAndB(int i, int j) {
double Ei = f(X[i]) - y[i];
double Ej = f(X[j]) - y[j];
double alpha_i_old = alphas[i], alpha_j_old = alphas[j];

double L, H;
computeLH(i, j, L, H);
if (L == H) return 0;

double eta = computeEta(i, j);
if (eta >= 0) return 0;

int changed = 0;
changed += updateAlphaJ(i, j, Ei, Ej, eta, L, H);
changed += updateAlphaI(i, j, alpha_j_old);

updateB(i, j, Ei, Ej, alpha_i_old, alpha_j_old);
return changed;
}

11. 训练方法

对应步骤:

  • 初始化并运行 SMO 算法来训练 SVM。
1
2
3
4
5
6
7
// 训练
void fit(const vector<vector<double>>& X, const vector<double>& y) {
// 初始化操作
initialize(X, y);
// 主循环,迭代优化 alpha 值
mainLoop();
}

测试

数据

数据如下(traindata.txt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2.0 3.0 1.0
1.0 5.0 1.0
2.5 7.0 1.0
1.5 2.0 1.0
3.0 3.5 1.0
2.0 4.5 1.0
3.5 5.0 1.0
1.0 6.0 1.0
4.0 2.5 -1.0
2.5 3.0 1.0
3.0 6.0 1.0
4.5 4.0 -1.0
5.0 5.5 -1.0
3.0 1.0 -1.0
3.6 0.5 -1.0

数据分布图

输出结果如下

result

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#include <iostream>
#include <vector>
#include <cmath>
#include <limits>
#include <fstream>
#include <random>
#include <algorithm>
using namespace std;

// 假设每个样本有2个输入特征和1个输出
const size_t INNODE = 2;
const size_t OUTNODE = 1;

struct Sample {
vector<double> in; // 输入特征
double out; // 输出(类标签)
};

namespace utils {
vector<double> getFileData(const string& filename) {
vector<double> res;
ifstream in(filename);
if (in.is_open()) {
double buffer;
while (in >> buffer) {
res.push_back(buffer);
}
in.close();
} else {
cout << "Error in reading " << filename << endl;
}
return res;
}

// 读取数据,includeOutData为true时,读取的数据包括输入和输出,否则只读取输入
vector<Sample> getData(const string& filename, bool includeOutData = true) {
vector<Sample> res;
vector<double> buffer = getFileData(filename);

size_t stepSize = includeOutData ? (INNODE + OUTNODE) : INNODE;

for (size_t i = 0; i < buffer.size(); i += stepSize) {
Sample tmp;
for (size_t t = 0; t < INNODE; t++) {
tmp.in.push_back(buffer[i + t]);
}
if (includeOutData && i + INNODE < buffer.size()) {
tmp.out = buffer[i + INNODE];
}
res.push_back(tmp);
}

return res;
}

}

class SVM {
public:
SVM(double C, double tolerance, int max_passes)
: C(C), tolerance(tolerance), max_passes(max_passes) {}
// 在SVM类中添加
vector<double> getWeights() const {
vector<double> weights(n, 0.0);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
weights[j] += alphas[i] * y[i] * X[i][j];
}
}
return weights;
}
// 预测方法
double predict(const vector<double>& x) {
double sum = 0.0;
for (int i = 0; i < m; i++) {
sum += alphas[i] * y[i] * kernel(x, X[i]);
}
return sum + b;
}

// 获取训练后的 alphas
const vector<double>& getAlphas() const {
return alphas;
}

// 获取训练后的偏置项 b
double getBias() const {
return b;
}
// 初始化操作
void initialize(const vector<vector<double>>& X, const vector<double>& y) {
this->X = X;
this->y = y;
m = X.size();
n = X[0].size();
alphas.resize(m, 0.0);
b = 0.0;
}
// 主循环,迭代优化 alpha 值
void mainLoop() {
int passes = 0;
while (passes < max_passes) {
int num_changed_alphas = iterateThroughDataSet();
if (num_changed_alphas == 0) passes++;
else passes = 0;
}
}
// 遍历数据集
int iterateThroughDataSet() {
int num_changed_alphas = 0;
for (int i = 0; i < m; i++) {
num_changed_alphas += examineExample(i);
}
return num_changed_alphas;
}
// 检查第 i 个样本是否满足 KKT 条件
int examineExample(int i) {
double Ei = f(X[i]) - y[i];
if ((y[i] * Ei < -tolerance && alphas[i] < C) || (y[i] * Ei > tolerance && alphas[i] > 0)) {
int j = selectJ(i, Ei);
if (j >= 0) {
return updateAlphasAndB(i, j);
}
}
return 0;
}
// 随机选择第二个乘子 j
int selectJ(int i, double Ei) {
int j = rand() % m;
while (j == i) {
j = rand() % m;
}
return j;
}
// 计算边界 L 和 H
void computeLH(int i, int j, double& L, double& H) {
if (y[i] != y[j]) {
L = max(0.0, alphas[j] - alphas[i]);
H = min(C, C + alphas[j] - alphas[i]);
} else {
L = max(0.0, alphas[i] + alphas[j] - C);
H = min(C, alphas[i] + alphas[j]);
}
}
// 计算 eta
double computeEta(int i, int j) {
return 2.0 * kernel(X[i], X[j]) - kernel(X[i], X[i]) - kernel(X[j], X[j]);
}
// 更新 alpha_j
int updateAlphaJ(int i, int j, double Ei, double Ej, double eta, double L, double H) {
double alpha_j_old = alphas[j];
alphas[j] -= y[j] * (Ei - Ej) / eta;
alphas[j] = min(H, max(L, alphas[j]));

if (abs(alphas[j] - alpha_j_old) < 1e-5) {
return 0; // 没有足够的改变
}
return 1; // 发生了改变
}
// 更新 alpha_i
int updateAlphaI(int i, int j, double alpha_j_old) {
double alpha_i_old = alphas[i];
alphas[i] += y[i] * y[j] * (alpha_j_old - alphas[j]);

return (alphas[i] != alpha_i_old); // 如果 alpha_i 改变了返回 1,否则返回 0
}
// 更新阈值 b
void updateB(int i, int j, double Ei, double Ej, double alpha_i_old, double alpha_j_old) {
double b1 = b - Ei - y[i] * (alphas[i] - alpha_i_old) * kernel(X[i], X[i])
- y[j] * (alphas[j] - alpha_j_old) * kernel(X[i], X[j]);
double b2 = b - Ej - y[i] * (alphas[i] - alpha_i_old) * kernel(X[i], X[j])
- y[j] * (alphas[j] - alpha_j_old) * kernel(X[j], X[j]);

if (0 < alphas[i] && alphas[i] < C) b = b1;
else if (0 < alphas[j] && alphas[j] < C) b = b2;
else b = (b1 + b2) / 2.0;
}
// 更新 alpha_i 和 alpha_j
int updateAlphasAndB(int i, int j) {
double Ei = f(X[i]) - y[i];
double Ej = f(X[j]) - y[j];
double alpha_i_old = alphas[i], alpha_j_old = alphas[j];

double L, H;
computeLH(i, j, L, H);
if (L == H) return 0;

double eta = computeEta(i, j);
if (eta >= 0) return 0;

int changed = 0;
changed += updateAlphaJ(i, j, Ei, Ej, eta, L, H);
changed += updateAlphaI(i, j, alpha_j_old);

updateB(i, j, Ei, Ej, alpha_i_old, alpha_j_old);
return changed;
}
// 训练
void fit(const vector<vector<double>>& X, const vector<double>& y) {
// 初始化操作
initialize(X, y);
// 主循环,迭代优化 alpha 值
mainLoop();
}

private:
double C;
double tolerance;
int max_passes;
vector<vector<double>> X;
vector<double> y;
vector<double> alphas;
double b;
int m, n;

double kernel(const vector<double>& x1, const vector<double>& x2) {
// 线性核函数
double result = 0.0;
for (int i = 0; i < n; i++) {
result += x1[i] * x2[i];
}
return result;
}

double f(const vector<double>& x) {
double sum = 0.0;
for (int i = 0; i < m; i++) {
sum += alphas[i] * y[i] * kernel(x, X[i]);
}
return sum + b;
}
};

int main() {
// 从文件读取数据
string filename = "traindata.txt";
vector<Sample> data = utils::getData(filename);

vector<vector<double>> X;
vector<double> y;

for (const auto& sample : data) {
X.push_back(sample.in);
y.push_back(sample.out);
}

// 创建 SVM 实例并使用读取的数据进行训练
SVM svm(1.0, 0.001, 10);
svm.fit(X, y);

// 输出训练后的模型参数
cout << "\nTrained Model Parameters:" << endl;
cout << "Weights:" << endl;
vector<double> weights = svm.getWeights();
for (const auto& w : weights) {
cout << w << " ";
}
cout << "\nBias (b): " << svm.getBias() << endl;

// 读取测试数据
string test_filename = "testdata.txt";
vector<Sample> test_data = utils::getData(test_filename, false);
vector<vector<double>> test_X;
for (const auto& sample : test_data) {
test_X.push_back(sample.in);
}

// 对测试数据进行预测并规范化结果
cout << "\nTest Data Classification Results (Normalized):" << endl;
for (const auto& x : test_X) {
double prediction = svm.predict(x);
// 规范化预测结果
double normalized_prediction = (prediction > 0.7) ? 1 : (prediction <-0.7 ? -1: prediction);
cout << "Test Data Point: (" << x[0] << ", " << x[1] << ") - Predicted Class: " << normalized_prediction << endl;
}

return 0;
}