【机器学习】感知机的C++实现
main函数
typedef vector<vector<double>> VVec;
typedef vector<double> Vec;
int main() {
Perceptron P; // 定义一个Perceptron类
VVec train_csv, test_csv; // VVec表示vector<vector<double>>
train_csv = P.csv_read("./Mnist/mnist_train.csv"); // 读取训练数据
test_csv = P.csv_read("./Mnist/mnist_test.csv"); // 读取测试数据
P.get_label(train_csv, 1); // 分离训练集及其label
P.get_label(test_csv, 0); // 分离测试集及其label
P.train(30); // 开始训练
cout << "acc: " << acc_rate << endl;
}
main函数内代码遵循读取数据、准备数据、开始训练、开始测试、输出结果这一顺序,以下依次实现上述功能。代码实现中将\(b\)作为\(w\)的一个维度。
Perceptron类定义
Perceptron类的大致定义,只需要完成以下函数,就能实现感知机算法。
class Perceptron {
public:
Vec w; // 权重与偏置合并在一起
Vec train_label, test_label; // 训练集与对应标签
VVec train_set, test_set; // 测试集与对应标签
VVec csv_read(string filename); // 读csv
void get_label(VVec& data); // 获取读取数据中的label项,返回数据集与对应label
void train(VVec& train_set, Vec& label, int itera); // 开始训练
double test(VVec& test_set, Vec& test_label); // 开始测试,输出测试效果
private:
double mul_vv(Vec& a, Vec& b); // 一维向量与一维向量的乘法运算,输出数字
Vec mul_vd(Vec& a, double b); // 一维向量与常数的乘法运算,输出一维向量
Vec add_vv(Vec& a, Vec& b); // 一维向量与一维向量的加法,输出一维向量
};
csv_read
VVec csv_read(string filename) {
ifstream inFile(filename); // 定义输入数据流
string lineStr;
VVec numArray; // 存储所有数值
while (getline(inFile, lineStr)) { // 开始遍历每一行,存成二维表结构
stringstream ss(lineStr);
string str;
Vec lineArray; // 存储每一行的数值
while (getline(ss, str, ',')) { // 按照逗号分隔
lineArray.push_back(stoi(str) / 255.);
}
numArray.push_back(lineArray);
}
return numArray;
}
get_label
void get_label(VVec& data, bool flag) { // 因为w和b合并在一起,所以每个样本后添加一个1
Vec cur_label;
for (auto& v : data) {
double target = v[0] * 255. > 4 ? 1 : -1; // 二分类
cur_label.push_back(target);
v[0] = 1; // 标签的位置置1,相当于添加了一个1
}
if (flag) {
this->train_label = cur_label;
this->train_set = data;
}
else {
this->test_label = cur_label;
this->test_set = data;
}
}
mul_vv
double mul_vv(Vec& a, Vec& b) { // 一维向量与一维向量的乘法运算,输出数字
double res = 0;
for (int i = 0; i < a.size(); i++) {
res += a[i] * b[i];
}
return res;
}
mul_vd
Vec mul_vd(Vec& a, double b) { // 一维向量与常数的乘法运算,输出一维向量
Vec res(a.size(), 0);
for (int i = 0; i < a.size(); i++) {
res[i] = a[i] * b;
}
return res;
}
add_vv
Vec add_vv(Vec& a, Vec& b) { // 一维向量与一维向量的加法,输出一维向量
Vec res(a.size(), 0);
for (int i = 0; i < a.size(); i++) {
res[i] = a[i] + b[i];
}
return res;
}
train
感知机使用一个超平面将输入样本空间分隔成两部分,一部分取为正例,另一部分取为负例。我们要解决的问题就是如何找到这个超平面。感知机的做法就是,在训练过程中,能够正确判断训练集时,感知机参数不变;当出错时,使用梯度下降法更新\(w\)和偏置\(b\)。
已知点到面的距离为\(\frac{wx + b}{||w||}\),我们设为\(F(x) =\frac{y (wx + b)}{||w||}\),当判断正确时,该距离为正,判断错误时,该距离为负,以此来区分判断错误的输入。当遇到这样的输入时,我们用梯度下降法更新感知机参数,由于\(w\)的二范数必定大于0,此时该距离对于参数\(w\)的导数为\(-y (x + b)\),参数\(b\)的导数为\(-y\)。
void train(int itera) {
int m = train_set.size();
int n = train_set[0].size();
w.assign(n, 0);
double h = 0.0001;
for (int i = 0; i < itera; i++) { // 开始迭代
for (int j = 0; j < m; j++) {
Vec xi = train_set[j]; // 当前训练数据,一维向量
double yi = train_label[j]; // 当前label
double res = yi * mul_vv(w, xi);
if (res <= 0) {
Vec w_delta = mul_vd(xi, h * yi);
w = add_vv(w, w_delta);
}
}
cout << "Round: " << i << " / " << itera << endl;
cout << "test_acc:" << test() << endl;
}
}
test
当\(y (wx + b)\)为负时,代表分类错误。
double test() {
int m = test_set.size();
int n = test_set[0].size();
double err_cnt = 0;
for (int i = 0; i < m; i++) {
Vec xi = test_set[i];
double yi = test_label[i];
double res = yi * mul_vv(w, xi);
if (res <= 0) err_cnt++;
}
double acc_rate = 1 - (err_cnt / m);
return acc_rate;
}