English Русский Español Deutsch 日本語 Português
preview
神经网络变得轻松(第十七部分):降低维度

神经网络变得轻松(第十七部分):降低维度

MetaTrader 5交易系统 | 2 九月 2022, 11:10
1 207 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容

概述

我们继续研究模型和无监督学习算法。 我们已经研究过数据聚类算法。 在本文中,我将探索与降维相关问题的解决方案。 本质上,这些就是在实践中被广泛使用的某些数据压缩算法。 我们研究其中这些算法之一的实现,看看如何用它来构建我们的交易模型。


1. 理解降维问题

每天、每时、每刻,在人类生活的各个领域都会提供大量信息。 随着当今世界信息技术的不断普及,人们试图保存和处理尽可能多的信息。 然而,对大量信息进行整理需要海量数据存储。 甚至,需要大量的计算资源来处理这些信息。 解决这个问题的可能方案之一是以更简洁的形式记录所提供的信息。 更有甚者,如果压缩表单保留了完整的数据上下文,则处理其的所需资源会更少。

例如,当我们处理 200*200 像素图像的形态识别时,每个像素都以颜色格式写入,占用 4 个字节内存。 每个像素均可用 1650 万种颜色之一来表示,这种能力对于这个问题来说有点过分。 在大多数情况下,如果我们把颜色层次降低到 16 或 32 色,模型性能不会受到影响。 在这种情况下,我们仅用 1 个字节来写入每个像素的颜色码。 当然,我们编写颜色矩阵会需要一次性开销,64 字节供 16 种颜色,而 128 字节供 32 种颜色。 将我们所有图像的大小缩小 4 倍,所要付出的代价并不太大。 实际上,这样的问题可用我们已知的数据聚类方法来解决。 不过,这也许并非最有效的方式。

降维技术的另一个应用领域是数据可视化。 例如,您有描述某些系统状态的数据,由 10 个参数表示。 您需要找到一种方式来把这些数据可视化。 2D 和 3D 图像对于人类感知是最优选。 那好,您可以创建若干个含有 2-3 个参数不同变化的截面。 但这不能提供系统状态的完整画面。 在大多数情况下,不同截面中的不同状态将融合到一个点。 但这些也许就是不同的状态。

因此,我们需要找到这样一种算法,帮助我们将所有系统状态从 10 个参数转换为二维或三维空间。 此外,算法应在划分系统状态的同时,保持系统状态的相对位置。 当然,它丢失的信息应该尽可能地少。

您也许会想:“这一切都很有趣,但交易的实际作用是什么?”,我们从终端中看看。 它提供了多少种指标? 好吧,其中许多可能都具有一定的数据相关性。 但它们当中的每一个都提供了至少一个从不同角度描述市场状况的参考值。 并且,如果我们将其乘以正在交易的金融产品数量会怎样? 甚至,指标和所分析时间帧的不同变化可以无限提升描述当前市场状态的参数量级。

当然,我们不会纠缠于把所有金融产品和所有可能的指标套用一个模型。 但无论如何,在搜索最合适的组合时,我们可以用到其中的许多组合。 这将令模型复杂化,并增加其训练时间。 因此,在维持最大信息的同时,降低初始数据的维度,我们既降低了模型训练成本,又缩减了制定决策的时间。 因此,对市场行为的反应可如闪电般的迅捷。 因此,交易将以最佳价格执行。

请注意,降维算法始终仅用于数据预处理。 这是因为它们只返回源数据的压缩形式。 随后数据被保存,或用于进一步处理。 这可以包括数据可视化,或由某些其它模型进行处理。

因此,为了构建一个交易系统,我们可用最小所需信息来描述当前市场状态,并采用一种降维算法对其进行压缩。 我们所应期望的是,减少处理流程,如此来消除一些噪声和相关数据。 然后,我们把降低的数据输入到我们的交易决策模型当中。

我希望这个思路是清晰的。 为了实现降维算法,我建议采用最流行之一的主成分分析法。 该算法已在解决各种问题方面证明了自己,并且可以在新数据上复现。 这能够减少传入数据,并将其传输到决策模型当中,从而生成实时交易决策。

2. 主成分分析法(PCA)

主成分分析是由英国数学家卡尔·皮尔逊(Karl Pearson)于 1901 年发明的。 自那时起,它已成功地应用于众多科学领域。

为了理解该方法的本质,我建议拿一项简单任务来示范,譬如有关将二维数据数组降维成向量。 从几何意义上来讲,这可以表示为平面上的点在直线上的投影。

在下图中,初始数据用蓝点表示。 有两个投影分别位于橙色和灰色线条上,并带有相应颜色的点。 如您所见,从初始点到其橙色投影的平均距离小于其到灰色投影的距离。 灰色投影存在重叠的点投影。 因此,橙色投影更为可取,因为它把所有单独的点分离,并且在降维(从点到其投影的距离)时丢失的数据更少。

这样一条线称为主成分。 这就是为什么该方法被称为主成分分析法

从数学角度来看,每个主成分都是一个数值向量,其大小等于原始数据的维度。 描述一个系统的原始数据向量,与相应的主成分向量的乘积,在直线上生成所分析状态的投影点。

取决于原始数据的维度和降维需求,可以有若干主成分,但不可超过原始数据维度。 渲染容积投影时,它们将有三个。 压缩数据时,允许的误差通常为至多丧失数据 1%。

主成分方法

直观上看,这类似于线性回归。 但这些是完全不同的方法,它们产生不同的结果。

线性回归表示一个变量与另一个变量的线性依赖关系。 此外,垂直于坐标轴的距离已被最小化。 这样的直线可以穿过平面的任何部分。

在主成分分析中,沿所有数轴的值是绝对独立和等效的。 垂直于直线但不垂直于轴的距离已被最小化。 主成分直线始终穿过原点,因此,在应用该方法之前,必须对所有初始数据进行归一化。 至少它们应以原点为中心。 换言之,我们需要在每个维度中将数据相对于0 点居中。

主成分分析法的另一个重要特征是其应用会返回主成分的正交向量矩阵。 这意味着所有主成分向量之间绝对不存在相关性。 这一事实对未来制定决策模型的整个学习过程具有积极影响,其输入所需接受的数据会大幅减少。

从数学角度来看,主成分分析方法可以表示为初始数据协方差矩阵的频谱分解。 并且协方差矩阵可以通过以下公式找到。

协方差矩阵公式

其中

  • C 是协方差矩阵,
  • X 是原始数据矩阵,
  • n 是源数据中的元素数量。

作为该操作的结果,我们得到协方差矩阵的平方。 其大小等于描述系统状态的特征数量。 特征值变化将沿着矩阵的主对角线定位。 矩阵的其它元素表示相应特征值对的协方差程度。

在下一阶段,我们需要对结果协方差矩阵进行奇异值分解。 矩阵的奇异值分解是一个相当复杂的数学过程。 但在 MQL5 中引入矩阵和矩阵运算大大简化了这一过程,因为针对矩阵的运算已经都实现了。 那好,我们立即处理奇异值分解的结果。

矩阵的奇异值分解

作为矩阵奇异值分解的结果,我们得到三个矩阵,其乘积等于原始矩阵。 第二矩阵 ∑ 是大小等于原始矩阵的对角矩阵。 沿着该矩阵的主对角线存在奇异数字,它们表示沿奇异向量轴的数值分散。 奇异数字是非负的,按降序排列。 矩阵的所有其它元素都等于 0。 因此,它通常表示为向量。

UV 是分别包含左奇异向量和右奇异向量的酉方阵。 U 矩阵的大小与原始矩阵具有相同的行数,而矩阵 V 具有与原始矩阵相同的列数。

在我们的案例中,当我们执行平方协方差矩阵的奇异值分解时,矩阵 UV 具有相同的大小。

为了降低维数,我们将使用矩阵 U。 由于奇异数字按照降序排列,我们可以简单地从矩阵 U 的第一列提取所需数字。 我们将新矩阵指代为矩阵 UR。 为了降低维数,我们可以简单地将原始数据矩阵乘以新创建的矩阵 UR

降维

这里出现的问题是:降低至何种程度是最佳的? 如果任务是数据可视化,则不会出现这样的问题。 取决于所需的投影,最终维度应在1 和 3 之间选择。 我们的任务是以最小的信息损失减少数据,并将其传递给另一个决策模型。 因此,主要准测是信息丢失的额度。

判定保留数据量的最佳选项,是计算奇异值与相应所用奇异向量的比率。

传输信息的比率

其中

  • k 是所用向量数
  • N 是奇异值的总数。

实际上,通常选择列数 k 的数量,如此即可令上述比值至少为 0.99。 这就相当于保留了 99% 的信息。

我们已经研究了一般的理论方面,现在,我们就可以开始实现方法了。


3. 利用 MQL5实现 PCA

为了实现主成分分析算法,我们将创建一个新类 CPCA,继承自 CObject 基类。 新类的代码将保存到 pca.mqh 文件。

我们将使用矩阵运算来实现这个类。 因此,模型训练结果,即矩阵 UR,将被保存到矩阵 m_Ureduce

此外,我们再声明三个局部变量。 这些是模型训练状态 b_studed 和两个向量 v_Meansv_ STDs,我们将保存算术平均值和标准差的值,用于进一步的数据归一化。

class CPCA : public CObject
  {
private:
   bool              b_Studied;
   matrix            m_Ureduce;
   vector            v_Means;
   vector            v_STDs;

在类构造函数中,给模型训练状态标志 b_studed 指定 false 值,并初始化矩阵 m_Ureduce 的大小为零。 保留类析构函数为空,因为我们不会在类内部创建任何嵌套对象。

CPCA::CPCA()   :  b_Studied(false)
  {
   m_Ureduce.Init(0, 0);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPCA::~CPCA()
  {
  }

接下来,我们将重新创建模型训练方法 Study。 该方法接收参数中的原始数据矩阵,并返回操作的逻辑结果。

如上所述,为了执行主成分分析,需要使用归一化的数据。 因此,在继续实现主方法算法之前,我们要用以下公式对初始数据进行归一化。 

数据归一化

利用矩阵运算来简化这项任务。 现在我们不需要创建循环系统。 为了找到所有特征值的算术平均值,我们可以调用 Mean 矩阵运算方法;在其中,我们指定维度的计数值。 作为操作的结果,我们立即得到了一个包含所有特征值的算术平均值的向量。

数据归一化公式的分母包含方差的平方根,它对应于标准偏差。 再来,我们能用矩阵运算。 STD 方法返回指定维度的标准偏差向量。 我们只需要添加一个小常数来消除除零错误。

将结果向量保存在相应的变量 v_Meansv_STDs 当中。 这种初始数据的归一化理应在模型训练阶段和操作阶段执行。

接下来,归一化数据。 为此目的,准备 矩阵 X,其大小等于原始数据相等。 实现一个循环,迭代次数等于源数据矩阵中的行数。

在循环体中,归一化初始数据,并将运算结果保存在先前创建的矩阵 X 当中。 利用矢量运算消除了创建嵌套循环的需要。

bool CPCA::Study(matrix &data)
  {
   matrix X;
   ulong total = data.Rows();
   if(!X.Init(total,data.Cols())
      return false;
   v_Means = data.Mean(0);
   v_STDs = data.STD(0) + 1e-8;
   for(ulong i = 0; i < total; i++)
     {
      vector temp = data.Row(i) - v_Means;
      temp /= v_STDs;
      X = X.Row(temp, i);
     }


在原始数据归一化之后,我们直接执行主成分分析算法。 如上所述,我们需要首先计算协方差矩阵。 致谢矩阵运算,这可以很容易地放入一行代码之中。 为了免创建不必要的对象,我覆该了矩阵 X 中的运算结果。

   X = X.Transpose().MatMul(X / total);

根据上述算法,下一个操作是协方差矩阵的奇异值分解。 作为该操作的结果,我们期望得到三个矩阵:左奇异向量、奇异值、和右奇异向量。 正如我们已经讨论过的,只有沿着主对角线的奇异值矩阵元素可以拥有非零值。 因此,为了在 MQL5 实现中节省资源,将返回奇异值向量,替代矩阵。

在调用函数之前,我们声明两个矩阵和向量,以便接收结果。 之后,我们可以调用 SVD 矩阵向量,将奇异值分解。 在参数中,我们传递给方法矩阵,和用于记录操作结果的向量。

   matrix U, V;
   vector S;
   if(!X.SVD(U, V, S))
      return false;

现在,我们就得到了奇异向量的正交矩阵,我们需要判定将原始数据降维到哪一等级。 作为一般做法,我们将至少保留原始数据中 99% 的信息。

按照上述逻辑,我们首先判定奇异值向量的所有元素的总和。 此外,请确保检查结果值是否大于 0。 它不能为负,因为奇异值不可为负。 此外,我们必须排除除零错误。

然后,我们计算奇异值向量值的累积和,并将所得向量除以奇异值的总和。

结果就是,我们将得到一个最大值等于 1 的递增值向量。

现在,为了判定所需的列数,我们需要从向量中找到大于或等于阈值信息保留值的第一个元素的位置。 在上述示例中,它是 0.99。 这就相当于保留了 99% 的信息。 

   double sum_total = S.Sum();
   if(sum_total<=0)
      return false;
   S = S.CumSum() / sum_total;
   int k = 0;
   while(S[k] < 0.99)
      k++;

我们仅需调整矩阵的大小,并将其内容转移到我们的类矩阵。 之后,切换模型训练标志,并退出该方法。

   if(!U.Resize(U.Rows(), k + 1))
      return false;
//---
   m_Ureduce = U;
   b_Studied = true;
   return true;
  }

在我们创建了模型训练方法,即判定了原始数据维度缩减矩阵之后,我们还可以创建 ReduceM 方法来缩减输入数据。 它将在参数中接收原始数据,并返回降维后的矩阵。

当然,输入数据必须与模型训练阶段采用的数据相比较。 此处我们谈论的是系统状态的数量和质量的描述特征,而不是观察的数量。

在方法开始时,我们创建一个控件模块,在其中检查模型训练标志。 在此,我们还检查初始数据矩阵中的列数(特征数)是否等于降维矩阵 m_Ureduce 中的行数。 如果有任何条件不满足,则退出该方法,并返回零大小的矩阵。

matrix CPCA::ReduceM(matrix &data)
  {
   matrix result;
   if(!b_Studied || data.Cols() != m_Ureduce.Rows())
      return result.Init(0, 0);

成功通过控制模块后,在执行降维之前对原始数据进行归一化。 归一化算法类似于我们在训练模型时讨论的算法。 唯一的区别是,这次我们无须计算算术平均值和标准差。 取而代之,我们采用在训练期间保存的相应向量。 因此,我们确保了新结果与训练期间获得的结果具有可比性。

   ulong total = data.Rows();
   if(!X.Init(total,data.Cols()))
      return false;
   for(ulong r = 0; r < total; r++)
     {
      vector temp = data.Row(r) - v_Means;
      temp /= v_STDs;
      result = result.Row(temp, r);
     }

在完成方法的算法之前,我们需要将归一化值矩阵乘以一个降维矩阵,并将运算结果返回给调用者。

   return result.MatMul(m_Ureduce);
  }

我们建立了训练模型的方法,降低了原始数据的维度。 感谢能调用矩阵运算,生成的代码非常简洁,我们不必深入研究数学知识。 但这是我们函数库中第一段使用矩阵运算编写的代码。 以前,我们在 CBufferDouble 对象中采用了动态数组。 因此,为了提供对象的兼容性,有必要创建一个接口,用于将数据从动态缓冲区传输到矩阵,反之亦然。

为了组织这个过程,我们将创建两个方法:FromBuffer FromMatrix。 第一个方法将接收含有动态数据缓冲区的参数,和一个描述系统状态的向量的大小。 它将把缓冲区内容传输到矩阵,并返回矩阵。

在方法体中,我们首先组织一个控件模块,在其中检查指向初始数据缓冲区对象指针的有效性。 然后,我们检查缓冲区大小是否为所分析系统的描述一个状态的向量的倍数。

matrix CPCA::FromBuffer(CBufferDouble *data, ulong vector_size)
  {
   matrix result;
   if(CheckPointer(data) == POINTER_INVALID)
     {
      result.Init(0, 0);
      return result;
     }
//---
   if((data.Total() % vector_size) != 0)
     {
      result.Init(0, 0);
      return result;
     }

如果所有检查都执行成功,则判定矩阵中的行数,并初始化结果矩阵。

   ulong rows = data.Total() / vector_size;
   if(!result.Init(rows, vector_size))
     {
      result.Init(0, 0);
      return result;
     }

接下来,组织一个嵌套循环系统,将动态缓冲区的所有内容转移到矩阵之中。

   for(ulong r = 0; r < rows; r++)
     {
      ulong shift = r * vector_size;
      for(ulong c = 0; c < vector_size; c++)
         result[r, c] = data[(int)(shift + c)];
     }
//---
   return result;
  }

一旦循环系统完成后,退出该方法,并将创建的矩阵返回给调用者。

第二个方法 FromMatrix 执行逆向操作。 在参数中,我们将数据矩阵输入到方法中,并在输出端接收动态数据缓冲区。

在方法主体中,我们首先创建动态数组的新对象,然后检查操作结果。

CBufferDouble *CPCA::FromMatrix(matrix &data)
  {
   CBufferDouble *result = new CBufferDouble();
   if(CheckPointer(result) == POINTER_INVALID)
      return result;

然后将动态数组的大小保留为足够大,从而能存储矩阵的全部内容。

   ulong rows = data.Rows();
   ulong cols = data.Cols();
   if(!result.Reserve((int)(rows * cols)))
     {
      delete result;
      return result;
     }

接下来,需要将矩阵的内容转移到动态数组。 此操作在两个嵌套循环的系统中执行。

   for(ulong r = 0; r < rows; r++)
      for(ulong c = 0; c < cols; c++)
         if(!result.Add(data[r, c]))
           {
            delete result;
            return result;
           }
//---
   return result;
  }

在所有循环操作成功完成后,我们退出该方法,并将创建的数据缓冲区对象返回给调用者。

这里应该注意,我们不保存指向创建对象的指针。 因此,任何与其状态监控相关的操作,以及在操作完成后从内存中删除它的操作,都必须在调用程序一侧组织。

我们创建处理向量的类似方法。 从缓冲区到向量的数据将调用重载的方法 FromBuffer 搬移。 逆向操作将在 FromVector 方法中执行。 构造方法的算法类似于上面给出的算法。 这些方法的完整代码在文后附件中提供。

创建数据传输方法之后,我们可以创建重载的模型训练方法,它将在参数中接收动态数据缓冲区,和一个系统状态描述向量的大小。 方法构造算法非常简单。 我们首先调用之前研究的方法 FromBuffer 把数据从动态缓冲区传输到矩阵。 然后,我们调用先前研究的模型训练方法,将结果矩阵传递给它。

bool CPCA::Study(CBufferDouble *data, int vector_size)
  {
   matrix d = FromBuffer(data, vector_size);
   return Study(d);
  }

我们为降维方法 ReduceM 创建一个类似的重载。 与重载的训练方法唯一区别在于,在方法参数中,我们只传递初始数据缓冲区,而不指定一个系统描述状态的向量大小。 这与一个事实有关,即此时模型已经被训练,状态描述向量的大小应该等于降维后矩阵的行数。

该方法的另一个不同之处在于,为了防止过度数据传输,我们首先检查模型是否已训练,以及缓冲区大小是否为状态描述向量大小的倍数。 只当所有检查均成功通过后,我们才调用数据传输方法。

matrix CPCA::ReduceM(CBufferDouble *data)
  {
   matrix result;
   result.Init(0, 0);
   if(!b_Studied || (data.Total() % m_Ureduce.Rows()) != 0)
      return result;
   result = FromBuffer(data, m_Ureduce.Rows());
//---
   return ReduceM(result);
  }

为了获得以动态数据缓冲区形式的降维矩阵,我们将创建 Reduce 的另外两个重载方法。 其一在参数中接收含有初始数据的动态数据缓冲区。 第二个则接收矩阵。 它们的代码如下所示。 

CBufferDouble *CPCA::Reduce(CBufferDouble *data)
  {
   matrix result = ReduceM(data);
//---
   return FromMatrix(result);
  }

CBufferDouble *CPCA::Reduce(matrix &data)
  {
   matrix result = ReduceM(data);
//---
   return FromMatrix(result);
  }

这看起来很奇怪,不过尽管方法参数不同,但它们的内容完全相同。 但这很容易由调用 educeM 重载方法来加以解释。

我们已经研究了类功能。 接下来,我们将需要创建处理文件的方法。 正如我们所记忆的,任何经过训练的模型都应该能够快速恢复其操作,以供后用。 一如既往,我们还是从数据保存方法 Save 开始。

但在继续构建数据保存方法算法之前,我们先看看类的结构,并思考应保存到文件里的内容。

在类的私密变量中,我们有一个模型训练标志 b_Studed、降维矩阵 m_Ureduce、和分别存储算术平均值 v_Means 及标准偏差 v_STDs 的两个向量,。 为了能够完全恢复模型的执行,我们需要保存所有这些元素。

class CPCA : public CObject
  {
private:
   bool              b_Studied;
   matrix            m_Ureduce;
   vector            v_Means;
   vector            v_STDs;
   //---
   CBufferDouble     *FromMatrix(matrix &data);
   CBufferDouble     *FromVector(vector &data);
   matrix            FromBuffer(CBufferDouble *data, ulong vector_size);
   vector            FromBuffer(CBufferDouble *data);

public:
                     CPCA();
                    ~CPCA();
   //---
   bool              Study(CBufferDouble *data, int vector_size);
   bool              Study(matrix &data);
   CBufferDouble     *Reduce(CBufferDouble *data);
   CBufferDouble     *Reduce(matrix &data);
   matrix            ReduceM(CBufferDouble *data);
   matrix            ReduceM(matrix &data);
   //---
   bool              Studied(void)  {  return b_Studied; }
   ulong             VectorSize(void)  {  return m_Ureduce.Cols();}
   ulong             Inputs(void)   {  return m_Ureduce.Rows();   }
   //---
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   //---
   virtual int       Type(void)  { return defUnsupervisedPCA; }
  };

当我们构建各种模型时,所有以前研究过的保存数据的方法,都会在参数中接收一个文件句柄,用于写入数据。 该类中的类似方法也不例外。 在方法主体中,我们立即检查所接收句柄的有效性。

bool CPCA::Save(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;

接下来,我们保存模型训练标志的值。 因为其状态决定是否需要保存其它数据。 如果模型尚未训练,则不需要保存空向量和矩阵。 在这种情况下,我们完成了该方法。

   if(FileWriteInteger(file_handle, (int)b_Studied) < INT_VALUE)
      return false;
   if(!b_Studied)
      return true;

如果模型已经过训练,我们则继续保存其余元素。 首先,我们保存降维矩阵。 在 MQL5 语言中,保存矩阵数据的函数尚未实现。 但是我们有一种方法可以写到数据缓冲区文件。 我们将受益于这个方法。

首先,我们将数据从矩阵转移到动态数据缓冲区。 然后,保存矩阵中的列数。 接下来,调用相关方法保存数据缓冲区。 注意,在把数据从矩阵转移到缓冲区的方法中,我们没有保存对象指针。 此外,我已经提到,与对象内存清除相关的任何操作都应该由调用者执行。 因此,在完成与数据保存相关的操作后,删除已创建对象。

   CBufferDouble *temp = FromMatrix(m_Ureduce);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(FileWriteLong(file_handle, (long)m_Ureduce.Cols()) <= 0)
     {
      delete temp;
      return false;
     }
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;

我们采用类似的算法来保存向量数据。

   temp = FromVector(v_Means);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;

   temp = FromVector(v_STDs);
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   if(!temp.Save(file_handle))
     {
      delete temp;
      return false;
     }
   delete temp;
//---
   return true;
  }

所有操作成功完成后,以 true 作为结果退出该方法。

Load 方法可从文件中恢复数据,数据顺序相同。 我们首先检查文件句柄的有效性,以便加载数据。

bool CPCA::Load(const int file_handle)
  {
   if(file_handle == INVALID_HANDLE)
      return false;

然后我们读取模型训练标志的状态。 如果模型尚未训练,则退出该方法,并获得肯定结果。 不需要执行与矩阵降维和向量相关的任何工作,因为它们将在模型训练期间被覆盖。 如果您尝试在训练前执行数据降维,该方法将检查训练标志的状态,并以否定结果完成。

   b_Studied = (bool)FileReadInteger(file_handle);
   if(!b_Studied)
      return true;

对于已训练模型,我们将首先创建一个动态缓冲对象。 然后,计算降维矩阵中的列数。 将降维矩阵的内容加载到数据缓冲区。

数据加载成功后,只需将动态缓冲区的上下文转移到我们的矩阵当中。

   CBufferDouble *temp = new CBufferDouble();
   if(CheckPointer(temp) == POINTER_INVALID)
      return false;
   long cols = FileReadLong(file_handle);
   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   m_Ureduce = FromBuffer(temp, cols);

采用类似的算法,我们将加载向量的内容。

   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   v_Means = FromBuffer(temp);

   if(!temp.Load(file_handle))
     {
      delete temp;
      return false;
     }
   v_STDs = FromBuffer(temp);

所有数据加载成功后,删除动态数据缓冲区对象,并以肯定结果退出该方法。

   delete temp;
//---
   return true;
  }

主成分方法类至此完毕。 附件中提供了所有方法和函数的完整代码。


4. 测试

主成分分析法的操作分两个阶段进行。 在第一次测试中,我训练了模型。 为此目的,我创建了 pca.mq5 智能系统,它基于我们在上一篇文章中研究的 kmeans.mq5 EA。 这些变化仅影响所用模型的对象,和 Train 函数训练模型。

同样,在程序开始时,判定训练区间的开始日期。

void Train(void)
  {
//---
   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year -= StudyPeriod;
   if(start_time.year <= 0)
      start_time.year = 1900;
   datetime st_time = StructToTime(start_time);

 然后我们下载所用指标的报价和数值。

   int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
      return;
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();

此后,我们将接收到的数据拆分到一个矩阵。 

   int total = bars - (int)HistoryBars;
   matrix data;
   if(!data.Init(total, 8 * HistoryBars))
     {
      ExpertRemove();
      return;
     }
//---
   for(int i = 0; i < total; i++)
     {
      Comment(StringFormat("Create data: %d of %d", i, total));
      for(int b = 0; b < (int)HistoryBars; b++)
        {
         int bar = i + b;
         int shift = b * 8;
         double open = Rates[bar]
                       .open;
         data[i, shift] = open - Rates[bar].low;
         data[i, shift + 1] = Rates[bar].high - open;
         data[i, shift + 2] = Rates[bar].close - open;
         data[i, shift + 3] = RSI.GetData(MAIN_LINE, bar);
         data[i, shift + 4] = CCI.GetData(MAIN_LINE, bar);
         data[i, shift + 5] = ATR.GetData(MAIN_LINE, bar);
         data[i, shift + 6] = MACD.GetData(MAIN_LINE, bar);
         data[i, shift + 7] = MACD.GetData(SIGNAL_LINE, bar);
        }
     }

调用模型训练方法。

   ResetLastError();
   if(!PCA.Study(data))
     {
      printf("Runtime error %d", GetLastError());
      return;
     }

训练成功后,将模型保存到一个文件当中,并调用智能系统完成其操作。

   int handl = FileOpen("pca.net", FILE_WRITE | FILE_BIN);
   if(handl != INVALID_HANDLE)
     {
      PCA.Save(handl);
      FileClose(handl);
     }
//---
   Comment("");
   ExpertRemove();
  }

完整的 EA 代码可在附件中找到。

依据过去 15 年历史数据,得到的 EA 性能结果,初始数据的维度从 160 个元素减少到 68 个元素。 也就是说,我们将源数据大小减少了近 2.4 倍,但只会丢失 1% 的信息。

在下一个测试阶段,我们采用预训练的主成分分析模型。 在减少源数据大小后,我们将类运算结果输入到完全连接的感知器之中。 为了这个测试,我们创建了 EA pca_net.mq5,它基于自上一篇文章的类似 EA kmeans_net.mq5。 感知器已依据过去两年的历史数据进行了训练。

基于简化数据的感知器训练结果

从图中可以看出,当在压缩数据上训练模型时,误差减少的趋势相当稳定。 在 55 个训练世代之后,误差大小尚未稳定。 这意味着如果我们继续训练,有可能会进一步降低误差。


结束语

在本文中,我们研究了采用无监督学习算法解决另一类问题:降维。 为了解决这种问题,我们创建了 CPCA 类,其中我们实现了主成分分析方法的算法。 这是一种非常有效的数据压缩方法,提供了可预测的信息丢失阈值。

在测试所创建的类时,我们将原始数据压缩了近 2.4 倍,但只是丢失了 1% 的信息。 这是一个非常好的结果,能够提高基于压缩数据训练的模型的效率。

此外,主成分法的一大特点是采用正交矩阵进行降维。 它把压缩数据中的特征值之间的相关性降低到几乎为 0。 它还提高了后续采用压缩数据的模型训练的效率。 第二次测试的结果确认了这一点。

同时,请注意不要试图采用主成分法来对抗模型过度拟合。 这是非常糟糕的做法。 在这种情况下,最好使用正则化方法。

这里还有一个来自一般做法的观察。 尽管在数据压缩过程中只丢失了十分少量的信息,但无论如何这种情况都会发生。 因此,仅当采用其它方法训练模型不能产生预期结果时,才建议采用降维法。

此外,我们还研究了新的矩阵运算。 特别感谢 MetaQuotes 在 MQL5 语言中实现此类操作。 在创建或为解决人工智能问题相关建模时,矩阵运算的使用大大简化了代码编写。

参考文献列表

  1. 神经网络变得轻松(第十四部分):数据聚类
  2. 神经网络变得轻松(第十五部分):利用 MQL5 进行数据聚类
  3. 神经网络变得轻松(第十六部分):聚类运用实践

本文中用到的程序

# 发行 类型 说明
1 pca.mq5 智能交易系统   训练模型的智能系统 
2 pca_net.mq5 EA
该智能系统测试给第二个模型传递数据
3 pсa.mqh 类库
实现主成分分析方法的函数库
4 kmeans.mqh  类库 实现 k-均值方法的函数库 
5 unsupervised.cl 代码库
OpenCL 程序代码库,实现 k-均值方法
6 NeuroNet.mqh 类库 用于创建神经网络的类库
7 NeuroNet.cl 代码库 OpenCL 程序代码库


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/11032

附加的文件 |
MQL5.zip (70.9 KB)
在莫斯科交易所(MOEX)里使用限价订单进行自动网格交易 在莫斯科交易所(MOEX)里使用限价订单进行自动网格交易
本文研究针对 MetaTrader 5 平台开发 MQL5 智能交易系统(EA),旨在能在 MOEX 上操作。 该 EA 采用网格策略,面向 MetaTrader 5 终端,并在 MOEX 上进行交易。 EA 包括了依据止损和止盈平仓,以及在某些市场条件下取消挂单。
学习如何基于 MFI 设计交易系统 学习如何基于 MFI 设计交易系统
这篇新文章出自我们的系列文章,是有关基于最流行的技术指标设计交易系统,它研究了一个新的技术指标 — 资金流动性指数(MFI)。 我们将详细学习它,利用 MQL5 开发一个简单的交易系统,并在 MetaTrader 5 中执行它。
DoEasy. 控件 (第 8 部分): 基准 WinForms 对象类别,GroupBox 和 CheckBox 控件 DoEasy. 控件 (第 8 部分): 基准 WinForms 对象类别,GroupBox 和 CheckBox 控件
本文研究创建 “GroupBox” 和 “CheckBox” WinForms 对象,以及开发 WinForms 对象类别的基准对象。 所有已创建对象仍然是静态的,即,它们无法与鼠标交互。
价格走势模型及其主要规定(第 1 部分):最简单的模型版本及其应用 价格走势模型及其主要规定(第 1 部分):最简单的模型版本及其应用
本文提供了数学上严格的价格运动和市场功能理论的基础。 到目前为止,我们还没有任何经过严格数学论证的价格走势理论。 取而代之的是,我们不得不基于经验假设进行处理,即价格在某种形态之后以某种方式移动。 当然,这些假设既没有得到统计数据的支持,也没有得到理论的支持。