从零实现的神经网络

· · 科技·工程

观察到 https://www.luogu.com.cn/article/3qk7k9hy 并未给出代码,于是我们尝试根据它和一些高等知识写一个简单的神经网络。我们将只使用 g++ 自带的库。(也就是说你可以在正赛考场上写它)

由于笔者很菜,如有错误,欢迎指出。

本文未使用任何生成式 AI 辅助。

我们的向量将会在行向量和列向量之间反复横跳,但这似乎并不关键。

数学部分

我们明确一下形式,假设神经网络有 n 层,编号从 0n-1,设第 i 层有 l_i 个节点。

记相邻两层之间的转移系数为 W_{i} 是一个 l_i\times l_{i+1} 的矩阵。

记第 i 层的偏置 B_i 是一个长度为 l_{i+1} 的向量。

记第 i 层的输出值为 R_i 是一个长为 l_i 的向量。

设激活函数为 f(x):\R\to \R 和它的导函数 df:\R\to \R。为了方便表述,f(X)df(X) 还可以表示对向量的每一个值分别应用该函数。

本文使用 \tanh 作为激活函数,其导函数为 1-\tanh^2

正向传播

正向传播是根据输入的值算出结果的过程。第 i+1 层的输出值可以根据上一层的输出值算出:

R1_{i+1}=R_iW_i+B_i\\[4pt] R_{i+1}=f\Big(R1_{i+1}\Big)

反向传播

由于笔者并不知道自动求导为何物,但是我们会手动求导:

假设已知损失函数对下一层所有输出值的梯度(的所有分量)

\frac{\partial loss}{\partial R_{i+1,y}}

我们需要求出

\frac{\partial loss}{\partial R_{i,x}},~ \frac{\partial loss}{\partial B_{i,y}},~ \frac{\partial loss}{\partial W_{i,x,y}}

我们先求出 loss 对激活之前的值的导数:

\frac{\partial loss}{\partial R1_{i,y}}=\frac{\partial loss}{\partial R_{i,y}}\frac{\partial R_{i,y}}{\partial R1_{i,y}}=\frac{\partial loss}{\partial R_{i,y}}df(R1_{i,y})

然后猜测三个式子:

\begin{align*} &\frac{\partial loss}{\partial R_{i,x}}=\sum_{y=0}^{l_{i+1}-1}W_{i,x,y}\frac{\partial loss}{\partial R1_{i+1,y}}\\[10pt] &\frac{\partial loss}{\partial B_{i,y}}=\frac{\partial loss}{\partial R1_{i+1,y}}\\[10pt] &\frac{\partial loss}{\partial W_{i,x,y}}=R_{i,x}\frac{\partial loss}{\partial R1_{i+1,y}} \end{align*}

感性理解是对的。写成矩阵和向量形式的化,第一个式子就是矩阵乘向量,第二个是对应位置赋值,第三个是张量积。

然后初始我们需要 loss 对最后一层(也就是输出值)的偏导数,这里看你用什么损失函数了,比如我用 \sum_{x=0}^{l_{n-1}-1}(R_{n-1,x}-res_x)^2 作为损失函数,那么初始梯度就是

\frac{\partial loss}{\partial R_{n-1,x}}=2R_{n-1,x}-2res_x

训练

随便搞一些样本,每个样本都跑一遍正向传播和反向传播,将梯度累加求和,然后向梯度的反方向调整参数。这个过程重复 2000 次。

如果初始参数都是一个数的话,一层的所有节点将会有相同的导数,导致值总是相同。因此初始需要随机赋值。随机数的值域可以从网上抄一些看起来比较优的。

由于我不想反复调参,所以我们训练的学习率由梯度各个分量归一化得到:

_Tp mx = _Tp();
for(size_t i = 0; i < n - 1; i++) {
    for(size_t j = 0; j < l[i + 1]; j++) mx = max(mx, abs(train.db[i][j]));
    for(size_t j = 0; j < l[i]; j++)
        for(size_t k = 0; k < l[i + 1]; k++) mx = max(mx, abs(train.dw[i][j][k]));
}
// _Tp K = train.alpha;
mx = max(0.02, mx);
_Tp K = train.alpha / mx; // K 才是真正的学习率,此处 alpha 取 1

代码部分

(文末有完整代码)

好了终于可以开始写代码了。首先把所有要用的头文件搞过来,然后生成随机数的 gen 和平方 sqr 可以自己写一个,后面的代码形式会好看一点?

#pragma once

/*
define NDEBUG to disable "assert"
*/

#include<iostream>
#include<fstream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cmath>
#include<random>
#include<cassert>
#include<ctime>
#include<iomanip>
using namespace std;

const double eps = 1e-6;

mt19937 rng((random_device){}());
inline double gen(double l, double r) {
    return (double)rng() / (double)0xffffffff * (r - l) + l;
}

template<typename _Tp>
inline _Tp sqr(_Tp x) { return x * x; }

然后我们需要实现一个低效且勾史的矩阵向量库,为了方便动态的调整每层的宽度,我们还需要手写内存管理,基本上就是在构造函数里面申请内存,在移动构造函数里面直接把对方的指针搞过来,没什么脑子:

#include "conf.h"

template<typename _Tp>
class matrix {

public:
    size_t n, m;
    size_t size;
    _Tp *data;

    inline _Tp *operator[](size_t index) { return data + (index * m); }
    inline const _Tp *operator[](size_t index) const { return data + (index * m); }
    inline matrix() : n(), m(), size(), data() {}
    inline matrix(size_t _n, size_t _m) : n(_n), m(_m), size(_n * _m) { data = new _Tp[size](); }
    inline matrix(matrix<_Tp> &&val) : n(val.n), m(val.m), size(val.size) { data = val.data; val.data = NULL; }
    inline matrix(const matrix<_Tp> &val) : n(val.n), m(val.m), size(val.size) { data = new _Tp[size](); for(size_t i = 0; i < size; i++) data[i] = val.data[i]; }
    inline ~matrix() { if(data != NULL) delete[] data; }

    inline void resize(size_t _n, size_t _m) { if(data != NULL) delete[] data; n = _n, m = _m, size = n * m; data = new _Tp[size](); }
    inline void clear() { if(data == NULL) data = new _Tp[size](); else for(size_t i = 0; i < size; i++) data[i] = _Tp(); }
    inline matrix<_Tp> &operator=(matrix<_Tp> &&val) { assert(n == val.n && m == val.m); if(data != NULL) delete[] data; data = val.data; val.data = NULL; return *this; }
    inline matrix<_Tp> &operator=(const matrix<_Tp> &val) { assert(n == val.n && m == val.m); if(data == NULL) data = new _Tp[size](); for(size_t i = 0; i < size; i++) data[i] = val.data[i]; return *this; }

};

template<typename _Tp>
class vec {

public:
    size_t n;
    _Tp *data;

    inline _Tp &operator[](size_t index) { return data[index]; }
    inline const _Tp &operator[](size_t index) const { return data[index]; }
    inline vec() : n(), data() {}
    inline vec(size_t _n) : n(_n) { data = new _Tp[n](); }
    inline vec(vec<_Tp> &&val) : n(val.n) { data = val.data; val.data = NULL; }
    inline vec(const vec<_Tp> &val) : n(val.n) { data = new _Tp[n](); for(size_t i = 0; i < n; i++) data[i] = val.data[i]; }
    inline ~vec() { if(data != NULL) delete[] data; }

    inline void resize(size_t _n) { if(data != NULL) delete[] data; n = _n; data = new _Tp[n](); }
    inline void clear() { if(data == NULL) data = new _Tp[n](); else for(size_t i = 0; i < n; i++) data[i] = _Tp(); }
    inline vec<_Tp> &operator=(vec<_Tp> &&val) { assert(n == val.n); if(data != NULL) delete[] data; data = val.data; val.data = NULL; return *this; }
    inline vec<_Tp> &operator=(const vec<_Tp> &val) { assert(n == val.n); if(data == NULL) data = new _Tp[n](); for(size_t i = 0; i < n; i++) data[i] = val.data[i]; return *this; }

};

然后就是实现各种运算,包括矩阵和向量的对应位置相加,相减,数乘,矩阵乘向量,向量乘矩阵,向量的对应位置相乘(elementwise_product)和张量积(tensor_product)。还有重载到 cout 方便我们输出调试。

由于我比较害怕写挂,加了一堆 assert 来判断两个操作数的大小是否匹配,指针是否不为空。这个 assert 占了很多代码。如果觉得慢可以在代码开头加一行

#define NDEBUG

代码如下:

template<typename _Tp>
class matrix {
    ...
    friend inline matrix<_Tp> operator+(const matrix<_Tp> &a, const matrix<_Tp> &b) {
        matrix<_Tp> res(a.n, a.m); assert(a.n == b.n && a.m == b.m && a.data && b.data);
        for(size_t i = 0; i < a.size; i++) res.data[i] = a.data[i] + b.data[i];
        return res;
    }
    friend inline matrix<_Tp> operator-(const matrix<_Tp> &a, const matrix<_Tp> &b) {
        matrix<_Tp> res(a.n, a.m); assert(a.n == b.n && a.m == b.m && a.data && b.data);
        for(size_t i = 0; i < a.size; i++) res.data[i] = a.data[i] - b.data[i];
        return res;
    }
    friend inline matrix<_Tp> operator-(const matrix<_Tp> &a) {
        matrix<_Tp> res(a.n, a.m); assert(a.data);
        for(size_t i = 0; i < a.size; i++) res.data[i] = -a.data[i];
        return res;
    }
    friend inline matrix<_Tp> operator*(const matrix<_Tp> &a, const matrix<_Tp> &b) {
        matrix<_Tp> res(a.n, b.m); assert(a.m == b.n && a.data && b.data);
        for(size_t k = 0; k < a.m; k++) {
            for(size_t i = 0; i < a.n; i++) {
                for(size_t j = 0; j < b.m; j++) {
                    res[i][j] += a[i][k] * b[k][j];
                }
            }
        }
        return res;
    }
    friend inline matrix<_Tp> operator*(const matrix<_Tp> &a, _Tp b) {
        matrix<_Tp> res(a.n, a.m); assert(a.data);
        for(size_t i = 0; i < a.size; i++) res.data[i] = a.data[i] * b;
        return res;
    }
    inline matrix<_Tp> &operator+=(const matrix<_Tp> &b) {
        assert(n == b.n && m == b.m && data && b.data);
        for(size_t i = 0; i < size; i++) data[i] += b.data[i];
        return *this;
    }

};

template<typename _Tp>
class vec {

public:
    ...
    inline friend vec<_Tp> operator+(const vec<_Tp> &a, const vec<_Tp> &b) {
        vec<_Tp> res(a.n); assert(a.n == b.n && a.data && b.data);
        for(size_t i = 0; i < a.n; i++) res[i] = a[i] + b[i];
        return res;
    }
    inline friend vec<_Tp> operator-(const vec<_Tp> &a, const vec<_Tp> &b) {
        vec<_Tp> res(a.n); assert(a.n == b.n && a.data && b.data);
        for(size_t i = 0; i < a.n; i++) res[i] = a[i] - b[i];
        return res;
    }
    inline friend vec<_Tp> operator-(const vec<_Tp> &a) {
        vec<_Tp> res(a.n); assert(a.data);
        for(size_t i = 0; i < a.n; i++) res[i] = -a[i];
        return res;
    }
    inline friend vec<_Tp> operator*(const vec<_Tp> &a, const matrix<_Tp> &b) {
        vec<_Tp> res(b.m); assert(a.n == b.n && a.data && b.data);
        for(size_t i = 0; i < a.n; i++) {
            for(size_t j = 0; j < b.m; j++) {
                res[j] += a[i] * b[i][j];
            }
        }
        return res;
    }
    inline friend vec<_Tp> operator*(const matrix<_Tp> &a, const vec<_Tp> &b) {
        vec<_Tp> res(a.n); assert(a.m == b.n && a.data && b.data);
        for(size_t i = 0; i < a.n; i++) {
            for(size_t j = 0; j < a.m; j++) {
                res[i] += a[i][j] * b[j];
            }
        }
        return res;
    }
    inline friend vec<_Tp> operator*(const vec<_Tp> &a, _Tp b) {
        vec<_Tp> res(a.n); assert(a.data);
        for(size_t i = 0; i < a.n; i++) res[i] = a[i] * b;
        return res;
    }
    inline friend vec<_Tp> elementwise_product(const vec<_Tp> &a, const vec<_Tp> &b) {
        vec<_Tp> res(a.n); assert(a.n == b.n && a.data && b.data);
        for(size_t i = 0; i < a.n; i++) res[i] = a[i] * b[i];
        return res;
    }
    inline friend matrix<_Tp> tensor_product(const vec<_Tp> &a,  const vec<_Tp> &b) {
        matrix<_Tp> res(a.n, b.n);
        for(size_t i = 0; i < a.n; i++) {
            for(size_t j = 0; j < b.n; j++) {
                res[i][j] = a[i] * b[j];
            }
        }
        return res;
    }

    inline vec<_Tp> &operator+=(const vec<_Tp> &b) {
        assert(n == b.n && data && b.data);
        for(size_t i = 0; i < n; i++) data[i] += b[i];
        return *this;
    }
    inline vec<_Tp> apply(_Tp (*func)(_Tp)) const {
        vec<_Tp> res(n); assert(data != NULL);
        for(size_t i = 0; i < n; i++) res[i] = func(data[i]);
        return res;
    }

};

template<typename _Tp>
inline ostream &operator<<(ostream &out, const matrix<_Tp> &val) {
    out << "mat" << "=\n";
    for(size_t i = 0; i < val.n; i++) {
        out << "[";
        for(size_t j = 0; j < val.m; j++) {
            out << val[i][j];
            if(j != val.m - 1) cout << ", ";
        }
        out << "]\n";
    }
    return out;
}
template<typename _Tp>
inline ostream &operator<<(ostream &out, const vec<_Tp> &val) {
    out << "vec" << "=[";
    for(size_t i = 0; i < val.n; i++) {
        out << val[i];
        if(i != val.n - 1) cout << ", ";
    }
    out << "]\n";
    return out;
}

好的以上这些相信大家自己都能写,没什么好说的,就是注意内存管理的问题。然后由于我们封装的比较完善,神经网络内部的实现就相对简单,只需要把式子抄一遍就行。

由于我们多个样本对平均梯度的贡献是需要累加的,所以我将神经网络的类 nnet 和累计梯度的类 trainTp 分开写了。本来是为了后面写随机化做铺垫,但是直接跑梯度下降效果就很好了,写法保留了。

下面先是内存管理,就是在构造函数里面把每层宽度 l 搞过来,然后对着 l 调用矩阵和向量的 resize 申请内存。

#include "conf.h"
#include "matrix.h"

template<typename _Tp>
class trainTp {

public:
    size_t n;
    size_t *l;
    vec<_Tp> *r;       // 正向传播得到的结果
    vec<_Tp> *r1;      // 激活之前的值
    vec<_Tp> *dr;      // dloss/dr  梯度
    vec<_Tp> *dr1;     // dloss/dr1 梯度

    size_t cnt;        // 学习次数,因为我们是自适应学习率,所以没用
    matrix<_Tp> *dw;   // dloss/dw  累计梯度
    vec<_Tp> *db;      // dloss/db  累计梯度
    _Tp alpha;         // 神秘常数

    inline trainTp(size_t _n, size_t *_l) : n(_n) {
        l = _l;
        r = new vec<_Tp>[n]();
        r1 = new vec<_Tp>[n]();
        dr = new vec<_Tp>[n]();
        dr1 = new vec<_Tp>[n]();
        dw = new matrix<_Tp>[n - 1]();
        db = new vec<_Tp>[n - 1]();
        for(size_t i = 0; i < n - 1; i++) {
            dw[i].resize(l[i], l[i + 1]);
            db[i].resize(l[i + 1]);
        }
        for(size_t i = 0; i < n; i++) {
            r[i].resize(l[i]);
            r1[i].resize(l[i]);
            dr[i].resize(l[i]);
            dr1[i].resize(l[i]);
        }
    }
    inline ~trainTp() {
        delete[] r;
        delete[] r1;
        delete[] dr;
        delete[] dr1;
        delete[] dw;
        delete[] db;
    }

    inline void setAlpha(_Tp _alpha) { alpha = _alpha; }
    inline void clearSum() {
        for(size_t i = 0; i < n - 1; i++) {
            dw[i].clear();
            db[i].clear();
        }
    }

};

template<typename _Tp>
class nnet {

public:
    size_t n;          // 层数
    size_t *l;         // 每层宽度
    matrix<_Tp> *w;    // 贡献系数
    vec<_Tp> *b;       // 偏置

    _Tp (*f)(_Tp);     // 激活函数
    _Tp (*df)(_Tp);    // df/dx

    inline nnet(size_t _n, size_t *_l, _Tp (*_f)(_Tp), _Tp (*_df)(_Tp)) : n(_n) {
        f = _f, df = _df; l = _l;
        w = new matrix<_Tp>[n - 1]();
        b = new vec<_Tp>[n - 1]();
        for(size_t i = 0; i < n - 1; i++) {
            w[i].resize(l[i], l[i + 1]);
            b[i].resize(l[i + 1]);
        }
    }
    inline ~nnet() {
        delete[] w;
        delete[] b;
    }

};

为了便于统计训练效果,我们封装一个统计类,支持插入,求 max, min, avg, sigma, sum


template<typename _Tp>
class resTp {
private:
    vector<_Tp> data;
public:
    inline void clear() { data.clear(); }
    // non-negative numbers only
    inline void insert(_Tp val) { data.push_back(val); }
    inline size_t size() { return data.size(); }
    inline _Tp sum() { _Tp res = 0; for(_Tp i : data) res += i; return res; }
    // 平方和
    inline _Tp sum2() { _Tp res = 0; for(_Tp i : data) res += i * i; return res; }
    inline _Tp Max() { _Tp res = -1; for(_Tp i : data) if(res < i) res = i; return res; }
    inline _Tp Min() { if(data.empty()) return -1; _Tp res = data.front(); for(_Tp i : data) if(i < res) res = i; return res; }
    inline _Tp Mid() { if(data.empty()) return -1; nth_element(data.begin(), data.begin() + data.size() / 2, data.end()); return data[data.size() / 2]; }
    inline _Tp avg() { if(data.empty()) return -1; return sum() / size(); }
    // 标准差
    inline _Tp sigma() { return sqrt(sum2() / size() - sqr(avg())); }
};

好的接下来可以写正向传播和反向传播了,还有训练和随机赋初值。直接对着数学部分的式子写一遍就行。


template<typename _Tp>
class nnet {

public:
    size_t n;          // 层数
    size_t *l;         // 每层宽度
    matrix<_Tp> *w;    // 贡献系数
    vec<_Tp> *b;       // 偏置

    _Tp (*f)(_Tp);     // 激活函数
    _Tp (*df)(_Tp);    // df/dx
    ...

    inline void forward(trainTp<_Tp> &train, const vec<_Tp> &start) {
        assert(start.n == l[0]);
        train.r[0] = start;
        for(size_t i = 0; i < n - 1; i++) {
            train.r1[i + 1] = train.r[i] * w[i] + b[i];
            train.r[i + 1] = train.r1[i + 1].apply(f);
        }
    }
    inline void backward(trainTp<_Tp> &train, const vec<_Tp> &ed) {
        train.cnt++;
        assert(ed.n == l[n - 1]);
        train.dr[n - 1] = (train.r[n - 1] - ed) * 2;
        for(size_t i = n - 2; i != (size_t)(-1); i--) {
            train.dr1[i + 1] = elementwise_product(train.dr[i + 1], train.r1[i + 1].apply(df));
            train.dr[i] = w[i] * train.dr1[i + 1];
            train.db[i] += train.dr1[i + 1];
            train.dw[i] += tensor_product(train.r[i], train.dr1[i + 1]);
        }
    }
    inline void train(trainTp<_Tp> &train, bool debug = false) {
        _Tp mx = _Tp();
        for(size_t i = 0; i < n - 1; i++) {
            for(size_t j = 0; j < l[i + 1]; j++) mx = max(mx, abs(train.db[i][j]));
            for(size_t j = 0; j < l[i]; j++)
                for(size_t k = 0; k < l[i + 1]; k++) mx = max(mx, abs(train.dw[i][j][k]));
        }
        // _Tp K = train.alpha;
        mx = max(0.02, mx);
        _Tp K = train.alpha / mx;
        if(debug) cout << "最终梯度:\n";
        for(size_t i = 0; i < n - 1; i++) {
            if(debug) cout << train.db[i] * K << train.dw[i] * K << endl;
            b[i] += -train.db[i] * K;
            w[i] += -train.dw[i] * K;
        }
    }
    // 随机赋初值,值域是从网上抄的,好像有什么意义
    inline void random() {
        for(size_t i = 0; i < n - 1; i++) {
            double val = sqrt(6.0 / (l[i] + l[i + 1]));
            for(size_t j = 0; j < l[i + 1]; j++) b[i][j] = gen(-val, val);
            for(size_t j = 0; j < l[i]; j++)
                for(size_t k = 0; k < l[i + 1]; k++) w[i][j][k] = gen(-val, val);
        }
    }
};

然后写一个 summary 函数用来计算一个样本的 loss。由于神经网络的计算结果就是存在 trainTp 里的,所以直接传一个 trainTp 和标准答案 ed 就行。

inline void summary(trainTp<_Tp> &train, const vec<_Tp> &ed, resTp<_Tp> &res) {
    _Tp sum = _Tp();
    for(size_t i = 0; i < l[n - 1]; i++) sum += sqr(train.r[n - 1][i] - ed[i]);
    res.insert(sum);
}

这样就都封装好了,我们只需要在主函数里面设置层数、宽度、激活函数。然后读取数据集,并调用训练函数就可以了。

#include "conf.h"
#include "matrix.h"
#include "net.h"

using namespace std;

double f(double x) { return tanh(x); }
double df(double x) { return 1 - sqr(tanh(x)); }

vec<double> bg[501];
vec<double> ed[501];

int main() {

    cout << fixed << setprecision(4);
    ifstream fin("dataset.in");

    size_t n = 5;
    size_t l[5] = {1, 4, 4, 4, 3};

    nnet<double> net(n, l, f, df);
    trainTp<double> tr(n, l);
    tr.setAlpha(1);
    net.random();

    cout << "初始参数:\n";
    for(size_t i = 0; i < n - 1; i++) {
        cout << net.b[i] << net.w[i] << endl;
    }

    cout << "开始训练\n";
    for(int i = 0; i < 500; i++) {
        bg[i].resize(l[0]), ed[i].resize(l[n - 1]);
        for(int j = 0; j < l[0]; j++) fin >> bg[i][j];
        for(int j = 0; j < l[n - 1]; j++) fin >> ed[i][j];
    }
    for(int cnt = 1; cnt <= 2000; cnt++) {
        if(cnt % 100 == 0) cout << "cnt: " << cnt << endl;
        tr.clearSum();
        for(int i = 0; i < 500; i++) {
            net.forward(tr, bg[i]);
            net.backward(tr, ed[i]);
        }
        net.train(tr, (cnt == 2000));
    }

    cout << "最终参数:\n";
    for(size_t i = 0; i < n - 1; i++) {
        cout << net.b[i] << net.w[i] << endl;
    }
    tr.clearSum();
    net.forward(tr, bg[0]);
    cout << "中间结果:\n";
    for(size_t i = 0; i < n; i++) {
        cout << tr.r[i];
    }

    resTp<double> res;
    bg[500].resize(l[0]), ed[500].resize(l[n - 1]);
    for(int i = 0; i < 500; i++) {
        for(int j = 0; j < l[0]; j++) fin >> bg[500][j];
        for(int j = 0; j < l[n - 1]; j++) fin >> ed[500][j];
        net.forward(tr, bg[500]);
        net.summary(tr, ed[500], res);
    }

    cout << "测试数据集: \tMax\tMin\tMid\tAvg\tsigma\t\n";
    cout << "          \t" << res.Max() << '\t' << res.Min() << '\t' << res.Mid() << '\t' << res.avg() << '\t' << res.sigma() << '\n';

    double te;
    while(cin >> te) {
        bg[500][0] = te;
        net.forward(tr, bg[500]);
        for(int j = 0; j < l[n - 1]; j++) cout << tr.r[n - 1][j] << ' '; cout << '\n';
    }

    return 0;
}

然后如果要训练的话,我们写个 generator,训练神经网络判断一个数是在 (-\infty,0],[0,0.5],(0.5,1] 的哪个区间。由于我们 \tanh 的输出是 (-1,1) 之间的,所以训练也让它输出 (-1,1)

#include<iostream>
#include<random>
using namespace std;

mt19937 rng((random_device){}());
int gen(int l, int r) { return (int)(rng() % (r - l + 1) + l); }
int n = 20;

int f(bool x) { return x ? 1 : -1; }

int main() {

    for(int i = 1; i <= 1000; i++) {
        double t = (double)gen(-100, 100) / 100;
        cout << t << ' ';
        cout << f(t < 0) << ' ';
        cout << f(0 <= t && t <= 0.5) << ' ';
        cout << f(0.5 < t && t <= 1) << '\n';
    }

    return 0;
}

在本地编译运行,解决这种简单的问题可以轻松让 loss 在测试数据集上收敛到 <10^{-4}(骗你的,样本太少,测试数据集和训练数据集都重合了)。开 O2 将会大幅提高训练速度。

另外本文还没有加入 GPU 和多线程、指令集的优化,以后可能会加。

你学会了吗

完整代码

练习

用上面的一坨通过 B3684 [语言月赛 202212] 不可以,总司令。

在 基于日线的 A 股量化交易模拟 中获得 1000 万分(bushi)。