神经网络实践:第一个神经元(基础篇)
「先跑通一个神经元」
在 MT5 里做神经网络,没必要一上来就堆多层。先实现一个神经元:输入加权求和再过激活函数,足以验证数据怎么从指标流进模型。 这个神经元接收若干输入 x[i],每个带权重 w[i],求和后加偏置 b,再交给激活函数。下面这段是最小可运行核心,复制到 MT5 脚本里就能编译。 [CODE]double Neuron(double &inputs[], double &weights[], double bias) { double sum=0; for(int i=0;i<ArraySize(inputs);i++) sum+=inputs[i]*weights[i]; sum+=bias; return 1.0/(1.0+MathExp(-sum)); // sigmoid }[/CODE] 逐行看:函数收三个引用,输入数组、权重数组、偏置值;循环把对应位相乘累加;最后一行是 sigmoid,把任意实数压到 0~1 之间。外汇与贵金属波动剧烈,用 sigmoid 输出概率时务必记住这是高风险标的,输出只代表倾向不是确定性。 实际验证时把 RSI(14) 的近 5 根数值塞进 inputs,权重随便给 0.2 起步,你能立刻在专家日志看到 0.48~0.52 这种中间概率——说明单神经元对震荡行情几乎无区分力,这正是下一步要加数据预处理的原因。
class="type">class="kw">double Neuron(class="type">class="kw">double &inputs[], class="type">class="kw">double &weights[], class="type">class="kw">double bias) { class="type">class="kw">double sum=class="num">0; for(class="type">int i=class="num">0;i<ArraySize(inputs);i++) sum+=inputs[i]*weights[i]; sum+=bias; class="kw">return class="num">1.0/(class="num">1.0+MathExp(-sum)); class=class="str">"cmt">// sigmoid }
被零截距绑住的神经元
上篇里手搓的那个简单神经元,其实藏着个不显眼的缺陷:它强制线性方程过笛卡尔原点 (0,0),也就是截距常数 b 被钉死为 0。最小二乘能拟合出斜率 a,但知识库里真实数据集往往需要 b ≠ 0 才能贴住数据分布。 之前为了补这个洞,我们分别用导数计算和矩阵计算去反推 a 和 b 的最优值,过程能跑通,但属于手工补丁。从这一节起,这类手算推导不再继续用,我们要换一套自动确定线性方程常数的路子。 上篇文章已经演示了怎么单独求斜率常数,建议你先在 MT5 里把那段代码跑一遍热手。接下来要处理的只比求斜率复杂一点点,但跨过这步后,后面搭网络会明显更顺手,也更能直接落地到行情建模。外汇与贵金属价格序列非线性强、杠杆风险高,用线性神经元做特征提取时务必用小样本先验证。
◍ 权重和偏差不过是斜率与截距
很多人一听到神经网络就头大,因为术语像雪崩一样砸过来。但剥开外壳看,「权重」就是线性方程里的斜率系数,「偏差」就是截距,没有任何玄学。 原文里反复强调:这两个词被换皮成了各种高大上的表达,底层数学却极其朴素。如果你在 MT5 里写过一个最简单的 y = w*x + b,就已经碰过神经元的核心了。 一个可验证的现象是:用一条割线去拟合散点数据,只调斜率不调截距,直线永远过不了你想要的区间;必须把 b 加进去,拟合才贴得住。外汇和贵金属行情里做这类线性映射,杠杆与跳空风险极高,任何拟合都只是概率倾向,不是方向保证。 所以别被「必须用某语言、某框架才能写神经网络」的话术吓住。在 MQL5 里,几行代码就能把权重和偏差跑出来,复杂度的门槛是自己加给自己的。
「从直线方程到单输入神经元」
把神经元想复杂之前,先看它最朴素的样子:一个输入、一个输出、一个权重加一个偏置,本质上就是 y = a·w + b 这条直线方程。原文里那套所谓「人工智能神经元」的雏形,第九行把训练值塞进变量 X,第十行做因式分解,算来算去就是初中直线方程,没有任何黑魔法。 完整跑完脚本后,训练矩阵里乘法因子是 2、交点该是 0,但神经元报出 weight = 1.9804357049081742、bias = 0.054422740828113325。第 15 行设的容差是 0.001,最终误差 0.0009994343288155726,刚好压在阈值下。这种偏差不是 bug,是概率指数的体现——它永远不会到 100%,因为近似误差不可避免。 别把卖课话术当真。纯靠拼代码片段就想在外汇或贵金属市场自动赚钱,概率趋近零;这类高风险市场里,唯一稳赚的是卖系统的人。但自己撸个小网络辅助决策(比如可视化趋势)完全可行,前提是真下功夫训。 单输入结构看着废,其实数字电路里的逆变器、缓冲器也是一进一出。把训练矩阵换一下:权重为负得到逆变器(输出反向),权重为正得到缓冲器(直通)。改二维数组里的数据,同一段代码就映射出不同行为,这正是神经网络的乐趣。 当然一个输入太局限,真实网络里神经元要多少输入有多少。先吃透这个最小单元,后面加输入才不会懵。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Cost(const class="type">class="kw">double w, const class="type">class="kw">double b) { class="type">class="kw">double err, fx, x; err = class="num">0; for (class="type">uint c = class="num">0; c < nTrain; c++) { x = Train[c][class="num">0]; fx = a * w + b; err += MathPow(fx - Train[c][class="num">1], class="num">2); } class="kw">return err / nTrain; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">double weight, ew, eb, e1, bias; class="type">int f = FileOpen("Cost.csv", FILE_COMMON | FILE_WRITE | FILE_CSV); Print("The first neuron..."); MathSrand(class="num">512); weight = (class="type">class="kw">double)macroRandom; bias = (class="type">class="kw">double)macroRandom;
梯度下降训练循环与代价函数实现
下面这段训练循环用最朴素的梯度下降去逼近线性映射。循环以 ULONG_MAX 为上限保护,只要当前代价 e1 大于容差 eps(这里设为 1e-3)就持续迭代,每次用差分估出权重和偏置的梯度并反向微调。
代价函数 Cost 对训练集 {0,0},{1,2},{2,4},{3,6},{4,8} 求预测值 a*w+b 与真实值的误差累积,样本数 nTrain 由 Train.Size()/2 得出为 5。肉眼可知目标是一条过原点的 y=2x 直线,因此收敛后 weight 应倾向贴近 2、bias 倾向贴近 0。
若传入了合法文件句柄 f,每轮会把迭代次数、权重、权重误差、偏置、偏置误差、总误差写成 CSV 行,方便你丢进 MT5 跑完直接画图看下降曲线。训练结束打印最终 weight / bias 与各误差项,开 MT5 新建脚本粘入即可验证。
外汇与贵金属市场高风险,这类线性拟合仅作机制演示,实盘信号请自行做样本外检验。
for(class="type">ulong c = class="num">0; (c < ULONG_MAX) && ((e1 = Cost(weight, bias)) > eps); c++) { ew = (Cost(weight + eps, bias) - e1) / eps; eb = (Cost(weight, bias + eps) - e1) / eps; weight -= (ew * eps); bias -= (eb * eps); if (f != INVALID_HANDLE) FileWriteString(f, StringFormat("%I64u;%f;%f;%f;%f;%f\n", c, weight, ew, bias, eb, e1)); } if (f != INVALID_HANDLE) FileClose(f); Print("Weight: ", weight, " Bias: ", bias); Print("Error Weight: ", ew); Print("Error Bias: ", eb); Print("Error: ", e1); class="macro">#define macroRandom(rand() / (class="type">class="kw">double)SHORT_MAX) class="type">class="kw">double Train[][class="num">2] { {class="num">0, class="num">0}, {class="num">1, class="num">2}, {class="num">2, class="num">4}, {class="num">3, class="num">6}, {class="num">4, class="num">8}, }; const class="type">uint nTrain = Train.Size() / class="num">2; const class="type">class="kw">double eps = class="num">1e-3; class="type">class="kw">double Cost(const class="type">class="kw">double w, const class="type">class="kw">double b) { class="type">class="kw">double err, fx, a; err = class="num">0; for (class="type">uint c = class="num">0; c < nTrain; c++) { a = Train[c][class="num">0]; fx = a * w + b;
◍ 单神经元梯度下降的训练落地
上面这段把单个神经元的训练主循环写死了:用固定种子 MathSrand(512) 起手,weight 和 bias 都从 rand()/SHORT_MAX 映射到 [0,1) 区间取初值,再靠 eps=1e-3 的数值微分去逼近 Cost 梯度。 循环终止条件很直白——要么迭代到 ULONG_MAX,要么 Cost(weight,bias) 掉到 eps 以下才停。每次迭代用 (Cost(w+eps,b)-e1)/eps 估权重偏导、对 bias 同理,然后各减 eps 倍梯度,属于最朴素的批量梯度下降。
| 顺手把每轮 c、weight、ew、bias、eb、e1 写进终端公共目录的 Cost.csv(FILE_COMMON | FILE_WRITE | FILE_CSV),你开 MT5 跑完直接去 FileOpen 路径翻这张表,就能看到误差是不是单调往下走。 |
|---|
训练集就 3 行:{0,1}、{1,0} 加一个未列全的第三组,nTrain 按 Train.Size()/2 算出来是 1.5 被截断成 1,这点数组尺寸若不改,Cost 里的 nTrain 归一会偏掉,复制代码前先把 Train 补成偶数行。
err += MathPow(fx - Train[c][class="num">1], class="num">2); } class="kw">return err / nTrain; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">double weight, ew, eb, e1, bias; class="type">int f = FileOpen("Cost.csv", FILE_COMMON | FILE_WRITE | FILE_CSV); Print("The first neuron..."); MathSrand(class="num">512); weight = (class="type">class="kw">double)macroRandom; bias = (class="type">class="kw">double)macroRandom; for(class="type">ulong c = class="num">0; (c < ULONG_MAX) && ((e1 = Cost(weight, bias)) > eps); c++) { ew = (Cost(weight + eps, bias) - e1) / eps; eb = (Cost(weight, bias + eps) - e1) / eps; weight -= (ew * eps); bias -= (eb * eps); if (f != INVALID_HANDLE) FileWriteString(f, StringFormat("%I64u;%f;%f;%f;%f;%f\n", c, weight, ew, bias, eb, e1)); } if (f != INVALID_HANDLE) FileClose(f); Print("Weight: ", weight, " Bias: ", bias); Print("Error Weight: ", ew); Print("Error Bias: ", eb); Print("Error: ", e1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define macroRandom(rand() / (class="type">class="kw">double)SHORT_MAX) class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Train[][class="num">2] { {class="num">0, class="num">1}, {class="num">1, class="num">0}, }; class=class="str">"cmt">//+------------------------------------------------------------------+ const class="type">uint nTrain = Train.Size() / class="num">2; const class="type">class="kw">double eps = class="num">1e-3; class=class="str">"cmt">//+------------------------------------------------------------------+