神经网络实践:第一个神经元·进阶篇
🧠

神经网络实践:第一个神经元·进阶篇

(2/3)· 权重与偏差只是斜率和截距的换皮,Sigmoid 才是让神经元活过来的关键一步

偏理论 第 2/3 篇
很多交易者刚接触神经网络就被权重、偏差这些词吓退,以为背后是天书级数学。其实它们只是线性方程里斜率和截距的别名,把简单东西包装复杂才是常见误区。本篇接着上篇草图,补上那个被忽略的常数缺陷,并引入真正让神经元有用的函数。

用宏和二维数组搭训练样本

在 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。

MQL5 / C++
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 前的线性版骨架,先跑通它再看停滞现象,比直接读理论直观:

MQL5 / C++
<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>] {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="number">class="num">0</span>, <span class="number">class="num">0</span>, <span class="number">class="num">0</span>},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="number">class="num">1</span>, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{<span class="number">class="num">1</span>, <span class="number">class="num">1</span>, <span class="number">class="num">1</span>},
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;};
<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)
{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> err;
&nbsp;&nbsp;&nbsp;&nbsp;err = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">uint</span> c = <span class="number">class="num">0</span>; c &lt; nTrain; c++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>);
&nbsp;&nbsp;&nbsp;&nbsp;<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>()
{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;w0, w1, err, ew0, ew1, eb, bias;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"The Mini Neuron..."</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">MathSrand</span>(<span class="number">class="num">512</span>);
&nbsp;&nbsp;&nbsp;&nbsp;w0 = (<span class="keyword">class="type">class="kw">double</span>)macroRandom;
&nbsp;&nbsp;&nbsp;&nbsp;w1 = (<span class="keyword">class="type">class="kw">double</span>)macroRandom;
&nbsp;&nbsp;&nbsp;&nbsp;bias = (<span class="keyword">class="type">class="kw">double</span>)macroRandom;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">ulong</span> c = <span class="number">class="num">0</span>; (c &lt; <span class="number">class="num">3000</span>) &amp;&amp; ((err = Cost(w0, w1, bias)) &gt; eps); c++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ew0 = (Cost(w0 + eps, w1, bias) - err) / eps;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ew1 = (Cost(w0, w1 + eps, bias) - err) / eps;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;eb&nbsp;&nbsp;= (Cost(w0, w1, bias + eps) - err) / eps;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;w0 -= (ew0 * eps);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;w1 -= (ew1 * eps);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bias -= (eb * eps);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">PrintFormat</span>(<span class="class="type">class="kw">string">"%I64u &gt; w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f"</span>, c, w0, ew0, w1, ew1, bias, eb, err);
&nbsp;&nbsp;&nbsp;&nbsp;}

◍ 单神经元梯度下降的 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 倾向负值。外汇与贵金属行情用类似结构建模时波动更噪,实盘验证属高风险操作。

MQL5 / C++
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 里的数字就能观察不同初值对收敛步数的影响;外汇与贵金属行情用这类模型预判前务必认清高风险,历史拟合不等于未来概率。

MQL5 / C++
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 边界模糊,实盘误触发概率偏高,仅适合当特征预筛层而非直接下单依据。

MQL5 / C++
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 试。观察它在不同行情里的输出边界,局限一定存在——这也是继续往下做多神经元或引入记忆结构的理由。

MQL5 / C++
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:
把重复推导交给小布
这些神经元参数演算和函数拟合的重复劳动,小布盯盘的 AIGC 模块已内置示意,打开对应品种页能看到简化版逻辑,你只需专注策略判断。

常见问题

因为当时常数 b 被设为零,直线只能交(0,0),而真实数据往往需要非零截距,拟合会偏差,本篇正是修这个缺陷。
目前小布内置的是逻辑示意与诊断,不输出完整 EA 源码;你可以参照本篇结构手写,再把回测诊断交给小布看盘口。
它把线性输出压缩到 0~1 区间,让神经元具备非线性表达能力,后续组合多层时才可能逼近复杂价格模式。
本质仍是斜率与截距的估计,样本外失效概率不低,贵金属与外汇杠杆高,信号仅作概率参考。
本篇起改用更通用的参数更新思路,旧方法在单神经元演示中有用,但面对多层网络效率偏低,倾向逐步淘汰。