神经网络实践:第一个神经元·进阶篇
(2/3)· 权重与偏差只是斜率和截距的换皮,Sigmoid 才是让神经元活过来的关键一步
用宏和二维数组搭训练样本
在 MT5 里做轻量随机化或监督学习原型,第一步是把样本和随机源先固化下来。下面这段直接定义了一个 macroRandom 宏,把 rand() 除以 SHORT_MAX 转成 0~1 之间的浮点随机数,后续做扰动或抽样时直接展开即可。 训练集用 double Train[][2] 硬编码了三条样本:{0,0}、{1,1},以及一条被注释占位的结构。二维第二列即标签,这种写法在调试感知机或线性拟合时很直观,改一行就能换数据。 nTrain 取 Train.Size()/2 得到样本数,这里 Size() 返回的是总行数 3,除以 2 得 1(整数截断),实际只够单样本迭代;eps 设 1e-3 作收敛阈值。开 MT5 把这段贴进脚本,打印 nTrain 你会看到输出 1,想跑全量得把除数改成 1。
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">0}, {class="num">1, class="num">1}, }; 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">//+------------------------------------------------------------------+
「用 sigmoid 救活多输入神经元的停滞」
给神经元塞进第二个输入后,训练曲线会在约一万次迭代附近趴平——成本函数要么停止下降,要么慢到肉眼难辨。根因不是梯度写错,而是输出端少了非线性激活,多输入场景下纯线性组合撞上了无法继续优化的停滞点。 补一个 sigmoid 就够了:它把负无穷到正无穷的加权和压进 0~1 区间,让权重更新重新获得有效的梯度方向。换上激活函数后,同一套梯度下降代码跑 2630936 次迭代才收敛进 1e-3 误差带,速度看着肉,主要因为每次迭代都 Print 刷屏。 把打印去掉、只在末尾做能力测试,神经元能学会 OR 门逻辑:{0,0}→0、{0,1}→1、{1,0}→1、{1,1}→1。这说明单神经元已能拟合比「两值是否相关」更复杂的映射关系,外汇或贵金属信号建模拿它做特征组合层时,也记得激活函数不是可选项。 下面这段代码是 MT5 里多输入+ sigmoid 前的线性版骨架,先跑通它再看停滞现象,比直接读理论直观:
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="preprocessor">class="macro">#define </span>macroRandom(<span class="functions">rand</span>() / (<span class="keyword">class="type">class="kw">double</span>)<span class="macro">SHORT_MAX</span>) <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> Train[][<span class="number">class="num">3</span>] { {<span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>}, {<span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>}, {<span class="number">class="num">1</span>, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>}, {<span class="number">class="num">1</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>}, }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">const</span> <span class="keyword">class="type">uint</span> nTrain = Train.Size() / <span class="number">class="num">3</span>; <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> eps = <span class="number">class="num">1</span>e-<span class="number">class="num">3</span>; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> Cost(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> w0, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> w1, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> b) { <span class="keyword">class="type">class="kw">double</span> err; err = <span class="number">class="num">0</span>; <span class="keyword">for</span> (<span class="keyword">class="type">uint</span> c = <span class="number">class="num">0</span>; c < nTrain; c++) err += <span class="functions">MathPow</span>(((Train[c][<span class="number">class="num">0</span>] * w0) + (Train[c][<span class="number">class="num">1</span>] * w1) + b) - Train[c][<span class="number">class="num">2</span>], <span class="number">class="num">2</span>); <span class="keyword">class="kw">return</span> err / nTrain; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnStart</span>() { <span class="keyword">class="type">class="kw">double</span> w0, w1, err, ew0, ew1, eb, bias; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"The Mini Neuron..."</span>); <span class="functions">MathSrand</span>(<span class="number">class="num">512</span>); w0 = (<span class="keyword">class="type">class="kw">double</span>)macroRandom; w1 = (<span class="keyword">class="type">class="kw">double</span>)macroRandom; bias = (<span class="keyword">class="type">class="kw">double</span>)macroRandom; <span class="keyword">for</span> (<span class="keyword">class="type">ulong</span> c = <span class="number">class="num">0</span>; (c < <span class="number">class="num">3000</span>) && ((err = Cost(w0, w1, bias)) > eps); c++) { ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; eb = (Cost(w0, w1, bias + eps) - err) / eps; w0 -= (ew0 * eps); w1 -= (ew1 * eps); bias -= (eb * eps); <span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f"</span>, c, w0, ew0, w1, ew1, bias, eb, err); }
◍ 单神经元梯度下降的 MT5 落地代码
下面这段 MQL5 脚本把「或运算」样本塞进一个单神经元模型,用最朴素的数值梯度下降逼近权重。训练集是 4 行 3 列:{0,0,0}、{0,1,1}、{1,0,1}、{1,1,1},目标输出正好是逻辑或。 宏 macroRandom 借 rand() 除以 SHORT_MAX 给出 0~1 随机数,macroSigmoid 是标准 sigmoid 激活。Cost 函数对 nTrain=4 条样本求 sigmoid 输出与标签的平方误差均值,eps 设 1e-3 同时充当学习率和微分步长。 OnStart 里先 MathSrand(512) 固定随机种子,初始 w0/w1/bias 由 macroRandom 给出;循环里用 (Cost(w+eps)-err)/eps 算三个参数的数值梯度,再各自减掉梯度乘 eps。终止条件是 err 落到 1e-3 以下或撞上 ULONG_MAX。 跑完你会看到 PrintFormat 逐轮刷出 w0、w1、bias 及对应误差,最终 w0 与 w1 都倾向收敛到接近 1 的正数,bias 倾向负值。外汇与贵金属行情用类似结构建模时波动更噪,实盘验证属高风险操作。
class="macro">#define macroRandom(rand() / (class="type">class="kw">double)SHORT_MAX) class="macro">#define macroSigmoid(a) (class="num">1.0 / (class="num">1 + MathExp(-a))) class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Train[][class="num">3] { {class="num">0, class="num">0, class="num">0}, {class="num">0, class="num">1, class="num">1}, {class="num">1, class="num">0, class="num">1}, {class="num">1, class="num">1, class="num">1}, }; class=class="str">"cmt">//+------------------------------------------------------------------+ const class="type">uint nTrain = Train.Size() / class="num">3; const class="type">class="kw">double eps = class="num">1e-3; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Cost(const class="type">class="kw">double w0, const class="type">class="kw">double w1, const class="type">class="kw">double b) { class="type">class="kw">double err; err = class="num">0; for (class="type">uint c = class="num">0; c < nTrain; c++) err += MathPow((macroSigmoid((Train[c][class="num">0] * w0) + (Train[c][class="num">1] * w1) + b) - Train[c][class="num">2]), class="num">2); class="kw">return err / nTrain; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">class="kw">double w0, w1, err, ew0, ew1, eb, bias; Print("The Mini Neuron..."); MathSrand(class="num">512); w0 = (class="type">class="kw">double)macroRandom; w1 = (class="type">class="kw">double)macroRandom; bias = (class="type">class="kw">double)macroRandom; for (class="type">ulong c = class="num">0; (c < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); c++) { ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; eb = (Cost(w0, w1, bias + eps) - err) / eps; w0 -= (ew0 * eps); w1 -= (ew1 * eps); bias -= (eb * eps); PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", c, w0, ew0, w1, ew1, bias, eb, err); } Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); Print("Error Weight class="num">0: ", ew0);
单神经元梯度下降的训练闭环
这段实现了一个最朴素的单神经元模型,用 OR 逻辑的真值表当训练集:输入 (0,0)→0、(0,1)→1、(1,0)→1、(1,1)→1,共 4 组样本。代价函数 Cost() 把所有样本经 Sigmoid 激活后的输出与标签之差平方求和再除以样本数,衡量当前权重下的拟合偏差。 训练主循环从 MathSrand(512) 固定随机种子开始,给 w0、w1、bias 赋 0~1 之间的初值,然后不断用数值微分估算梯度:每个参数在 +eps(1e-3)处算一次代价,减当前代价再除以 eps,得到近似偏导 ew0、ew1、eb。 参数更新就是 w0 -= ew0*eps 这种最简单的一阶步进,直到代价低于 eps 或跑到 ULONG_MAX 才停。实跑时若种子取 512,迭代次数通常落在几百到几千量级,最终 err 会压到 1e-3 以下,说明这个小网络把 OR 表学下来了。 把代码贴进 MT5 脚本直接编译运行,改 MathSrand 里的数字就能观察不同初值对收敛步数的影响;外汇与贵金属行情用这类模型预判前务必认清高风险,历史拟合不等于未来概率。
class="macro">#define macroRandom(rand() / (class="type">class="kw">double)SHORT_MAX) class="macro">#define macroSigmoid(a) (class="num">1.0 / (class="num">1 + MathExp(-a))) class="type">class="kw">double Train[][class="num">3] { {class="num">0, class="num">0, class="num">0}, {class="num">0, class="num">1, class="num">1}, {class="num">1, class="num">0, class="num">1}, {class="num">1, class="num">1, class="num">1}, }; const class="type">uint nTrain = Train.Size() / class="num">3; const class="type">class="kw">double eps = class="num">1e-3; class="type">class="kw">double Cost(const class="type">class="kw">double w0, const class="type">class="kw">double w1, const class="type">class="kw">double b) { class="type">class="kw">double err; err = class="num">0; for (class="type">uint c = class="num">0; c < nTrain; c++) err += MathPow((macroSigmoid((Train[c][class="num">0] * w0) + (Train[c][class="num">1] * w1) + b) - Train[c][class="num">2]), class="num">2); class="kw">return err / nTrain; } class="type">void OnStart() { class="type">class="kw">double w0, w1, err, ew0, ew1, eb, bias; class="type">ulong count; Print("The Mini Neuron..."); MathSrand(class="num">512); w0 = (class="type">class="kw">double)macroRandom; w1 = (class="type">class="kw">double)macroRandom; bias = (class="type">class="kw">double)macroRandom; for (count = class="num">0; (count < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); count++) { ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; eb = (Cost(w0, w1, bias + eps) - err) / eps; w0 -= (ew0 * eps); w1 -= (ew1 * eps); bias -= (eb * eps); } PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", count, w0, ew0, w1, ew1, bias, eb, err); Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); Print("Error Weight class="num">0: ", ew0); Print("Error Weight class="num">1: ", ew1); Print("Error Bias: ", eb); Print("Error: ", err); }
「用两层循环逼出 OR 神经元的输出曲面」
这段测试代码把权重 w0、w1 与偏置 bias 固定后,遍历输入 p0、p1 的 0/1 组合,直接打印 sigmoid 激活值。OR 逻辑只有 4 种输入,循环边界写死为 2,跑一遍就能看见神经元怎么把 (0,0) 压到接近 0、把其余三组的输出顶向 1。 在 MT5 里新建一个脚本,把上文训练得到的 w0、w1、bias 填进去,宏 macroSigmoid 必须已定义,否则编译期就报错。PrintFormat 用 %f 输出浮点,你能拿到具体概率值而非仅逻辑判定,方便对比不同学习率下权重的收敛差异。 外汇与贵金属行情里拿这种极简感知机做信号,样本噪声会让 0/1 边界模糊,实盘误触发概率偏高,仅适合当特征预筛层而非直接下单依据。
Print("Testing the neuron..."); for (class="type">uchar p0 = class="num">0; p0 < class="num">2; p0++) for (class="type">uchar p1 = class="num">0; p1 < class="num">2; p1++) PrintFormat("%d OR %d IS %f", p0, p1, macroSigmoid((p0 * w0) + (p1 * w1) + bias)); class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 从单神经元到 GPU 加速的取舍
前面几篇我们把一个只有两个输入、五个参数、一个输出的神经元在 MT5 里跑通了。它结构简单,但调出能用的权重方程花了不少试错时间,这说明哪怕最基础的感知机,落地也比看上去麻烦。 想进一步压训练耗时,OpenCL 调用 GPU 是现成路径。不过在模型规模还小的时候,CPU 跑这几行前向传播足够快,过早上 GPU 反而增加环境配置负担。若你打算做多隐藏层或种群进化训练,一块独立显卡会把某些迭代过程显著提速。 下面这段 Python 是前向计算的核心,虽不在 MT5 内执行,但逻辑和 MQL5 端神经元一致:双输入经隐藏层 tanh 激活,再加权输出到 (-1,1)。 别把预测准当盈利 有读者用 NEAT 训了 1300 代、每代 20 个基因组,模型对下一根蜡烛方向判断很激进且看似准确,却没能转正收益。外汇与贵金属属高风险品种,信号命中率高不代表期望为正,过拟合和滑点会吞掉边缘优势。 附件里的完整代码可以直接丢进 MT5 试。观察它在不同行情里的输出边界,局限一定存在——这也是继续往下做多神经元或引入记忆结构的理由。
class="kw">import os class="kw">import csv class="kw">import math class="kw">import random class="kw">import json from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime # ----------------------------------------- # UTILS:激活和简单前传 # ----------------------------------------- def tanh(x): class="kw">return math.tanh(x) def forward_pass(genome, inputs): """ genome = { &class="macro">#x27;hidden_weights&class="macro">#x27;: 2D list, &class="macro">#x27;hidden_bias&class="macro">#x27;: 1D list, &class="macro">#x27;output_weights&class="macro">#x27;: list, &class="macro">#x27;output_bias&class="macro">#x27;: class="type">float, &class="macro">#x27;fitness&class="macro">#x27;: class="type">float } inputs: list of class="num">5 floats Returns: single class="type">float in(-class="num">1, class="num">1) """ hidden_activations = [] for h in range(len(genome[&class="macro">#x27;hidden_bias&class="macro">#x27;])): z = sum(inputs[i] * genome[&class="macro">#x27;hidden_weights&class="macro">#x27;][h][i] for i in range(len(inputs))) z += genome[&class="macro">#x27;hidden_bias&class="macro">#x27;][h] hidden_activations.append(tanh(z)) z_out = sum(hidden_activations[h] * genome[&class="macro">#x27;output_weights&class="macro">#x27;][h] for h in range(len(hidden_activations))) z_out += genome[&class="macro">#x27;output_bias&class="macro">#x27;] class="kw">return tanh(z_out) def interpret_output(output): """ Convert the Tanh output to discrete values: if output >= class="num">0.5 => class="num">1 (Buy) if output <= -class="num">0.5 => -class="num">1 (Sell) otherwise => class="num">0 (Hold) """ if output >= class="num">0.5: class="kw">return class="num">1 elif output <= -class="num">0.5: