【机器学习】感知机的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;
}
posted @ 2021-03-13 00:18  tmpUser  阅读(291)  评论(0编辑  收藏  举报