机器学习方法

在矩阵和向量的内置方法当中,有几个是在机器学习任务中所需要的,尤其是在神经网络实现中。

顾名思义,神经网络是诸多神经元(即基本计算单元)的集合。称它们为“基本”,是因为它们执行比较简单的计算:通常一个神经元有一系列应用于某些输入信号的权重系数,此后信号的加权和被馈入函数(即非线性转换器)。

使用激活函数放大弱信号并限制过强的信号,防止进入饱和状态(实数计算溢出)。然而,最重要的在于,非线性赋予网络新的计算能力,能够解决更复杂的问题。

基础神经网络

基础神经网络

神经网络的强大之处体现为将大量神经元组合起来并在它们之间建立连接。通常,神经元被组织为层(可类比矩阵或向量),包括具有递归(循环)连接的层,并且还可能有效果各个相同的激活函数。这就能够使用各种算法分析体积性数据,尤其是通过找出它们当中隐藏的模式来分析。

请注意,若每个神经元中没有非线性特性,多层神经网络可等效表示为单层网络,其系数由所有层的矩阵积获得(Wtotal = W1 * W2 * ... * WL,其中,1..L 是层的编号)。这将是一个简单的线性加法器。因此,激活函数的重要性是有数学依据的。

一些最著名的激活函数

一些最著名的激活函数

神经网络的主要分类标准之一是根据使用的学习算法分为有监督学习网络和无监督学习网络。有监督学习网络要求人类专家向原始数据集提供所需的输出(例如交易系统状态的离散标记,或者暗示价格增量的数字指标)。无监督网络自行标识数据中的群集。

在任何情况下,训练神经网络的任务是找出使训练和测试样本误差最小化的参数,为此就需要使用损失函数:该函数可定性和定量估计目标网络应答和接收到的网络应答之间的误差。

神经网络之所以能广泛应用,最关键因素包括:选择对于信息性和相互独立的预测因子(经分析的特征)、根据学习算法具体细节进行数据转换(规范化和清理)以及网络架构和规模优化。请注意,使用机器学习算法并不保证成功。

在这里,我们不会涉及神经网络的理论、它们的分类以及要解决的典型任务。这一主题太过于广泛。感兴趣的读者可以参考 mql5.com 函数及其它来源的文章。

MQL5 提供三种机器学习方法,这些方法已成为矩阵和向量 API 的一部分。

  • Activation 计算激活函数值
  • Derivative 计算激活函数的导数值
  • Loss 计算损失函数值

激活函数的导数能够基于在学习过程中变化的模型误差来高效更新模型参数。

前两个方法将结果写到传递的向量/矩阵,并返回成功指示器(truefalse),而损失函数返回一个数字。我们提供它们的原型(我们将二者均标记在 object<T> 类型下, matrix<T>vector<T>):

bool object<T>::Activation(object<T> &out, ENUM_ACTIVATION_FUNCTION activation)

bool object<T>::Derivative(object<T> &out, ENUM_ACTIVATION_FUNCTION loss)

T object<T>::Loss(const object<T> &target, ENUM_LOSS_FUNCTION loss)

某些激活函数允许使用第三个自变量(可选)来设置参数。

请参阅 MQL5 文档,了解 ENUM_ACTIVATION_FUNCTION 枚举中支持的激活函数和 ENUM_LOSS_FUNCTION 枚举中损失函数列表。

作为介绍性示例,我们来探讨分析真实分时报价流的问题。有些交易员将分时报价视为垃圾噪点,而有些交易员却实行基于分时报价的高频交易。有一种假设是高频算法原则上对大资本玩家有优势,因为高频算法完全基于软件对价格信息的处理。基于此,我们将提出这样一个假设:由于做市商的当前活动的机器人的作用,在分时报价流中有一种短期记忆效应。然后,可以利用机器学习法找出这一依赖性,并预测若干将来分时报价。

机器学习始终涉及提出假设,为假设合成模型以及在实践中试验这些假设。显然,并非总是能够获得富有成效的假设。这是一个漫长的试错过程,失败是改进和新思路的源泉。

我们将使用最简单的神经网络类型之一:双向联想记忆 (BAM)。这样一种网络只有两层:输入和输出层。在输出中会形成某种响应(关联)以响应输入信号。层大小可以各不相同。当大小相同时,结果是霍普菲尔德网络。

全连接双向联想记忆

全连接双向联想记忆

使用此类网络,我们将比较 N 个最近前面分时报价和 M 个后面预测的分时报价,从近期历史到给定深度构建训练样本。分时报价将以价格涨跌幅的正负值形式馈入网络,并转换为二进制值 [+1, -1](二进制信号是 BAM 和霍普菲尔德网络中简洁形式的编码)。

BAM 最重要的优势是几乎即时(相比大多数其它迭代方法)的学习过程,该过程包括计算权重矩阵。我们将在下面给出公式。

然而,这种简洁性也有劣势:BAM 容量(它能记住的图像数量)限于最小的层大小,且前提是满足训练样本向量中 +1 和 -1 的特殊分布条件。

因此,对于我们的情况来说,网络会对训练样本中的所有报价序列进行泛化,随后在常规运行过程中,根据新输入的分时报价序列,回滚到某个存储的模式。实际效果取决于很多因素,包括网络规模和设置、当前分时报价流的特征等等。

由于假定分时报价流仅有短期记忆,因此最好是实时或接近实时重新训练网络,因为训练实际上会简化为若干个矩阵运算。

因此,为了使网络记住关联图像(对我们情况来说,指分时报价流的过去和将来),需要以下方程:

W = Σi(AiTBi)

其中 W 是网络的权重矩阵。对输入向量 Ai 和对应输出向量 Bi的所有成对积求和。

然后,当网络正在运行时,我们将输入图像提供给第一层,对其应用将 W 矩阵,从而激活第二层,在第二层中计算每个神经元的激活函数。然后,使用转置的 W T 矩阵,信号传播回第一层,也在神经元中应用激活函数。此刻,输入图像不再到达第一层,即自由振荡过程在网络中继续。该过程持续到网络神经元中的信号变化稳定下来(即变得小于某个预定义值)。

在此状态中,网络的第二层包含找到的关联输出图像 - 预测。

我们在 MatrixMachineLearning.mq5 脚本中实施这一机器学习场景。

在输入参数中,你可以设置从历史记录中请求的上次分时报价总数 (TicksToLoad),以及设置分配其中多少用于测试 (TicksToTest)。相应地,模型(权重)将基于 (TicksToLoad - TicksToTest) 分时报价。

input int TicksToLoad = 100;
input int TicksToTest = 50;
input int PredictorSize = 20;
input int ForecastSize = 10;

此外,在输入变量中,选择输入向量(已知分时报价数 PredictorSize)和输出向量(将来分时报价数 ForecastSize)的大小。

OnStart 函数的开头请求分时报价。在这里,我们只处理 Ask 价格。但是你也可以随交易量一起添加 Bid Last 过程。

void OnStart()
{
   vector ticks;
   ticks.CopyTicks(_SymbolCOPY_TICKS_ALL | COPY_TICKS_ASK0TicksToLoad);
   ...

我们将分时报价拆分为训练集和测试集。

   vector ask1(n - TicksToTest);
   for(int i = 0i < n - TicksToTest; ++i)
   {
      ask1[i] = ticks[i];
   }
   
   vector ask2(TicksToTest);
   for(int i = 0i < TicksToTest; ++i)
   {
      ask2[i] = ticks[i + TicksToLoad - TicksToTest];
   }
   ...

为计算价格增量,我们使用具有附加向量 {+1, -1} 的 Convolve 方法。请注意,具有增量的向量将比原始向量少 1 个元素。

   vector differentiator = {+1, -1};
   vector deltas = ask1.Convolve(differentiatorVECTOR_CONVOLVE_VALID);
   ...

根据 VECTOR_CONVOLVE_VALID 算法的卷积意味着仅完全向量重叠才会被纳入考虑范围(即较小的向量随较大向量顺序偏移,不超出其边界)。其它类型的卷积允许向量仅与一个元素或元素的一半重叠(在此情况下,其余元素超出对应向量,卷积值表现出边界效应)。

为将增量的连续值转换为单位脉冲(正和负,取决于向量的初始符号),我们使用辅助函数 Binary(这里未显示):它返回向量的新副本,其中每个元素为 +1 或 -1。

   vector inputs = Binary(deltas);

基于收到的输入系列,我们使用 TrainWeights 函数计算 W 神经网络权重矩阵。我们将稍后了解该函数的结构。目前我们需要注意的是,向该函数传递了 PredictorSizeForecastSize 参数,从而能够分别根据输入和输出 BAM 层的大小将连续分时报价系列拆分为成对输入和输出向量集。

   matrix W = TrainWeights(inputsPredictorSizeForecastSize);
   Print("Check training on backtest: ");   
   CheckWeights(Winputs);
   ...

网络训练完成后,我们立即在训练集上检查其精度:只是为了确保网络已经过训练。这由 CheckWeights 函数实现。

然而,更重要的是检查网络如何处理不熟悉的测试数据。为此,我们求第二个向量 ask2 的微分并将其二值化,然后将其发送至 CheckWeights

   vector test = Binary(ask2.Convolve(differentiatorVECTOR_CONVOLVE_VALID));
   Print("Check training on forwardtest: ");   
   CheckWeights(Wtest);
   ...
}

现在我们来熟悉 TrainWeights 函数,我们在其中定义矩阵 A 和 B,以将向量从传递的输入系列(从 data 向量)“切下”。

template<typename T>
matrix<TTrainWeights(const vector<T> &dataconst uint predictorconst uint responce
   const uint start = 0const uint _stop = 0const uint step = 1)
{
   const uint sample = predictor + responce;
   const uint stop = _stop <= start ? (uint)data.Size() : _stop;
   const uint n = (stop - sample + 1 - start) / step;
   matrix<TA(npredictor), B(nresponce);
   
   ulong k = 0;
   for(ulong i = starti < stop - sample + 1i += step, ++k)
   {
      for(ulong j = 0j < predictor; ++j)
      {
         A[k][j] = data[start + i * step + j];
      }
      for(ulong j = 0j < responce; ++j)
      {
         B[k][j] = data[start + i * step + j + predictor];
      }
   }
   ...

每个连续的 A 模式从数量等于 predictor 的连续分时报价获得,而对应的将来模式从以下 response 元素获得。只要总数据量允许,该窗口就会一次一个元素地向右移动,形成更多新的图像对。图像按行编号,而其中的分时报价按列编号。

接下来,我们应为权重矩阵 W 分配内存,并使用矩阵方法填充:我们使用 Outer 按顺序从 A 到 B 乘以行,然后执行矩阵求和。

   matrix<TW = matrix<T>::Zeros(predictorresponce);
   
   for(ulong i = 0i < k; ++i)
   {
      W += A.Row(i).Outer(B.Row(i));
   }
   
   return W;
}

CheckWeights 函数可为神经网络执行类似操作,其权重系数以现成形式在第一个 W 自变量中传递。训练向量的大小提取自 W 矩阵本身。

template<typename T>
void CheckWeights(const matrix<T> &W
   const vector<T> &data
   const uint start = 0const uint _stop = 0const uint step = 1)
{
   const uint predictor = (uint)W.Rows();
   const uint responce = (uint)W.Cols();
   const uint sample = predictor + responce;
   const uint stop = _stop <= start ? (uint)data.Size() : _stop;
   const uint n = (stop - sample + 1 - start) / step;
   matrix<TA(npredictor), B(nresponce);
   
   ulong k = 0;
   for(ulong i = starti < stop - sample + 1i += step, ++k)
   {
      for(ulong j = 0j < predictor; ++j)
      {
         A[k][j] = data[start + i * step + j];
      }
      for(ulong j = 0j < responce; ++j)
      {
         B[k][j] = data[start + i * step + j + predictor];
      }
   }
   
   const matrix<Tw = W.Transpose();
   ...

矩阵 A 和 B 在此情况下不是用于计算 W,而是充当用于测试的向量的“供应者”。我们还需要一个 W 的转置副本,用以计算从第二个网络层到第一个网络层的返回信号。

网络允许的瞬态过程迭代次数(超过该次数后进行收敛)取决于 limit 常量。

   const uint limit = 100;
   
   int positive = 0;
   int negative = 0;
   int average = 0;

需要变量 positivenegativeaverage 来计算成功和不成功预测的统计数据,以评估训练质量。

此外,网络在测试模式对的循环中激活,并采用其最终响应。每个下一输入向量被写入向量 a,并且输出层 b 以零填充。之后,为从 ab 的信号传输使用矩阵 W 以及应用激活函数 AF_TANH 启动迭代,并且为从 ba的反馈信号同样使用 AF_TANH 启动迭代。该过程一直持续到 limit 循环(这不太可能),或者直至满足收敛条件,在该条件下, ab 神经元状态向量几乎不会改变(这里我们使用 Compare 法以及来自前一迭代的 xy 向量)。

   for(ulong i = 0i < k; ++i)
   {
      vector a = A.Row(i);
      vector b = vector::Zeros(responce);
      vector xy;
      uint j = 0;
      
      for( ; j < limit; ++j)
      {
         x = a;
         y = b;
         a.MatMul(W).Activation(bAF_TANH);
         b.MatMul(w).Activation(aAF_TANH);
         if(!a.Compare(x0.00001) && !b.Compare(y0.00001)) break;
      }
      
      Binarize(a);
      Binarize(b);
      ...

达到稳定状态后,我们使用 Binarize 函数(其类似于先前提到的 Binary 函数,但会原位更改向量的状态)将神经元的状态从连续(实数)转为二进制 +1 和 -1。

现在,我们只需要统计具有目标向量的输出层中的匹配项数量。为此,执行向量的标量乘法。结果为正数,表示正确猜想的分时报价数超过不正确数。总命中计数累积在“平均数”中。

      const int match = (int)(b.Dot(B.Row(i)));
      if(match > 0positive++;
      else if(match < 0negative++;
      
      average += match// 0 in match means 50/50 precision (i.e. random guessing)
   }

所有测试样本的循环完成后,我们显示统计数据。

   float skew = (float)average / k// average number of matches per vector
   
   PrintFormat("Count=%d Positive=%d Negative=%d Accuracy=%.2f%%"
      kpositivenegative, ((skew + responce) / 2 / responce) * 100);
}

该脚本还包括 RunWeights 函数,该函数表示神经网络针对来自最后 predictor 分时报价的在线向量的一次工作运行(由其权重矩阵 W 运行)。该函数将返回一个带有将来分时报价估计数的向量。

template<typename T>
vector<TRunWeights(const matrix<T> &Wconst vector<T> &data)
{
   const uint predictor = (uint)W.Rows();
   const uint responce = (uint)W.Cols();
   vector a = data;
   vector b = vector::Zeros(responce);
   
   vector xy;
   uint j = 0;
   const uint limit = LIMIT;
   const matrix<Tw = W.Transpose();
   
   for( ; j < limit; ++j)
   {
      x = a;
      y = b;
      a.MatMul(W).Activation(bAF_TANH);
      b.MatMul(w).Activation(aAF_TANH);
      if(!a.Compare(x0.00001) && !b.Compare(y0.00001)) break;
   }
   
   Binarize(b);
   
   return b;
}

OnStart 结束时,我们暂停执行 1 秒钟(以便等待一定概率的新分时报价),请求最后 PredictorSize + 1 分时报价数(别忘了 +1 以便区分),然后在线作出它们的预测。

void OnStart()
{
   ...
   Sleep(1000);
   vector ask3;
   ask3.CopyTicks(_SymbolCOPY_TICKS_ALL COPY_TICKS_ASK0PredictorSize + 1);
   vector online = Binary(ask3.Convolve(differentiatorVECTOR_CONVOLVE_VALID));
   Print("Online: "online);
   vector forecast = RunWeights(Wonline);
   Print("Forecast: "forecast);
}

以默认设置对星期五晚上的 EURUSD 运行该脚本得到以下结果。

Check training on backtest: 
Count=20 Positive=20 Negative=0 Accuracy=85.50%
Check training on forwardtest: 
Count=20 Positive=12 Negative=2 Accuracy=58.50%
Online: [1,1,1,1,-1,-1,-1,1,-1,1,1,-1,1,1,-1,-1,1,1,-1,-1]
Forecast: [-1,1,-1,1,-1,-1,1,1,-1,1]

交易品种和时间未提及,因为市场情况会极大地影响算法适用性以及特定网络配置。当市场开放时,每次运行脚本时,你将获得新的结果,因为越来越多的分时报价进来。这是与短期记忆形式假设一致的预期行为。

我们可以看到,训练精度可接受,但测试数据方面明显降低,可能低于 50%。

到这里,我们顺利从编程进入了科研领域。MQL5 中内置的机器学习工具套件允许你实施神经网络和分析器的很多其它的配置,有不同的交易策略和原理用于准备初始数据。