本文是之前发表的文章 "在 MetaTrader 5 中使用自组织特征映射 (Kohonen 映射)" 的续篇。对素材进行了大比例修订, 并适于在一些项目里更容易的应用。通常, 文章旨在帮助初学者和有经验的程序员将 Kohonen 映射的神经网络算法连接到他们的项目中, 并编辑算法主体。在本文里只使用了一段范形样本, 但重点放在使用各种代码的例子。
自组织特征映射 (SOM) 是单层i网络, 其每个神经元都与 n-维输入向量 (范形) 的所有组件连接。输入向量 (范形) — 是集簇接受对象之一的描述。
在自组织特征映射里执行训练无需主管。为了训练目的引入了竞争机制。当发送一个范形网络到输入, 与输入范形稍有不同的神经元载体获胜。以下比例适用于优胜者神经元:
此处:
最常用的距离是欧氏空间。
在此实现中, 神经元胜者的搜索是在 CSOM_Net_Base 类的 BestMatchingNode 函数里执行。
围绕神经元胜者形成学习半径和群落。学习半径的定义就是在此迭代里哪个神经元作为主题训练。在训练之初其值最大, 随着训练迭代增长数值降低, 所以只有在最终阶段落入学习半径的神经元才是胜出者。
学习半径范围内的神经元权重适应于 Kohonen 规则:
此处:
学习半径之外的神经元权重并不适合。在此实现中, 权重适应是在 CSOMNode 类的 AdjustWeights 函数里。
ni(k) 训练速率分为两个部分:
此处 A 和 B 是选择的常量。
此函数与训练循环次数成反比, 所以, 在初始阶段, 训练的速度很高且半径很长, 范形平均。在训练的末期, 权重最终调整到输入参数。
一般情况下, 一个特定神经元的适应动态范围可以表示为下降梯度:
当训练 Kohonen 网络时, 会发生称为 "死神经元" 的问题。初始权重系数远离输入范形的神经元在训练程度内永远不会赢得竞争胜利。在此实现中, Kohonen 训练算法的系统问题将用两个修订来解决。
首先, 从训练集合中随机选择的范形函数被修正。类 C_PRNG_UD 用于替代 rand()/N 但不保证所有数值将会出现在 N 次迭代中。它保证了选择的随机范形均匀分布。其二, 神经元的权重使用随机值初始化, 但只能在范形能够遇到的范围内。
这样, 即使我们遇到从不会赢得竞争的神经元 — "死神经元", 那么, 至少, 它们将会留在获胜者的临近, 并将接受训练,在集簇期间扮演一个范形之间的桥梁角色。
我们现在将解释 Kohonen 映射的软件实现。实际实现基于两个措施: 范形的维度 (m_dimension) 和节点的总数 (m_total_nodes)。不过, 直观卡片呈现为三维, 此处 m_total_nodes 是折叠成 m_xcells * m_ycells 长方形。所以, 节点 存储有关权重和位置的信息。
class CSOMNode { protected: //--- 节点域的坐标 int m_x1; int m_y1; int m_x2; int m_y2; //--- 节点中心坐标 double m_x; double m_y; //--- 节点权重 double m_weights[]; ...
类 CSOMNode 合成到 CSOM_Net_Base 基类形成一个 CSOMNode m_som_nodes[] 类的一维样本数组。这是实际的 Kohonen 网络。其它函数只是服务于训练和网络开发。
在以前的文章里强调过范形样本的多样性, 并展示了接口多样性图形。在本文, 只有一个范例, 但涵盖了连通性的五种变化。我已经修订了源代码, 以便能够轻松连接到另一个项目。实现划分为五个类。
class CSOM_Net_Base class CSOM_Net_Data : public CSOM_Net_Base class CSOM_Net_Train : public CSOM_Net_Data class CSOM_Net_Img : public CSOM_Net_Train class CSOM_Net_Demo : public CSOM_Net_Img
声明揭示了类的承接级联。这种方式, 在连接的项目中若没有某些函数的需求, 也可以不连接子类, 因此下列函数可被砍除。
下面是公有函数列表:
class CSOM_Net_Base { public: //--- Kohonen 网络的共有节点 CSOMNode *public_node; //--- 函数发送指定索引集合的节点到 public_node 共有域 void GetNode(int ind){ public_node=m_som_nodes[ind].GetObjPointer(); }; //--- 函数返回范形维度 int GetDimension(){return(m_dimension);}; //--- 函数用于从文件中加载网络 bool DownloadNet(string file_name); //--- 函数用来基于指定携带掩码的向量搜索最佳节点 int BestMatchingNode(const double &vector[]); //--- 函数用来基于指定截取维度的向量搜索最佳节点 int BestMatchingNode(const double &vector[],int dimension); //--- 函数填充位掩码并用来搜索相似的范形节点 (掩码确定哪些字段进行搜索) bool InitSetByteMap(int num,bool value); }; class CSOM_Net_Data : public CSOM_Net_Base { public: //--- 函数用于从指定文件中加载训练数据 bool LoadPatternDataFromFile(string filename); //--- 函数用来在训练集合里添加向量 void AddVectorToPatternsSet(double &vector[],string title); //--- 函数用于从训练集合里拷贝范形 bool GetPatterns(int ind_pattern,double &vector[]); //--- 函数返回来自训练集合的范形名称 string GetTitlesPatterns(int ind_pattern); //--- 函数返回范形总数 int GetTotalsPatterns(); }; class CSOM_Net_Train : public CSOM_Net_Data { public: //--- 函数初始化网络, 获取参数 void InitParameters(int iterations,int xcells,int ycells); //--- 函数用于训练网络 void Train(); //--- 函数用于保存网络到文件中 void SaveNet(string file_name); //--- 虚函数 (实体描述在 CSOM_Net_Img 子类中) virtual void Render(){}; virtual void ShowBMP(bool back){}; }; class CSOM_Net_Img : public CSOM_Net_Train { public: //--- 函数初始化网络, 获取参数 void InitParameters(int iterations,int xcells,int ycells, int bmpwidth,int bmpheight, bool p_HexagonalCell,bool p_ShowBorders,bool p_ShowTitles, int p_ColorScheme,int p_MaxPictures); //--- 函数用来显示网络状态 void Render(); //--- 函数用来在图表上显示 bmp 图像 void ShowBMP(bool back); //--- 函数用于保存资源到 bmp 文件中 void SaveBMP(); //--- 逆初函数, 从图表上移除 BMP 图像 void NetDeinit(); }; class CSOM_Net_Demonstration : public CSOM_Net_Img { public: //--- 函数 - 使用子类来增加机会的例子 void ShowTrainPatterns(); };
附加文件包括五个带选项的使用 Kohonen 映射的例子。这些例子的序号按照类级联的顺序。我们将依次分析这些例子。
Sample1_SOM_Net_Base 示意如何从参数里指定的二进制文件里加载一个经过训练的网络 (无需扩展名):
input string SOM_Net="SOM\\SOM_Net";
扩展名 somnet 将会自动添加。这有助于防止混乱, 所以不适于算法识别的文件不予加载。
网络加载是在 DownloadNet(string file_name) 函数里发生。跟进函数实体的进一步详解, 文件 somnet 的格式变得清晰。头部由三个 双精度 类型存储单元构成 (就像其它从文件里获取的数组), 它们写在文件的开始。第一个字段保存范形的维度加上六。需要六个字段来存储节点坐标。但是, 由于六是常数值, 我们可以很容易地从数组头部的第一个字段里扣除六来获取范形的维度。第二和第三字段保存变量 m_xcells 和 m_ycells — 分别为网络可视范围的 x 和 y 大小。乘以它们您可以获取在网络中的节点数目。
这之后可以逐个获取网络数据向量, 直到整个网络均被加载。
让我们继续这个例子。在网络加载之后, 范形vector[] 由常数填充。以常量进行初始化只是作为例子。函数 BestMatchingNode 查找最像似的节点索引。然后节点被发送到公有部分用来安全访问 CSOMNode 类的函数。
我们将会更多关注 BestMatchingNode 函数。它有三种应用方式。当调用 BestMatchingNode(const double &vector[]) 重载时不进行掩码初始化, 搜索的执行将遍历所有向量字段, 因为函数将用自身初始化掩码, 即, 它将允许搜索遍历所有字段。如果预先调用 InitSetByteMap(int num,bool value) 为每个字段初始化掩码, 则搜索将在是否由这些字段进行过滤之间切换。基于不完整信息搜索节点的能力就以这种方式实现。如果使用了其它 BestMatchingNode(const double &vector[],int dimension) 重载, 则过滤将按照发送到函数的 dimension 参数的数量。
第二个示例 Sample2_SOM_Net_Data 展示了以下函数扩展的连接 — CSOM_Net_Data 类。该类声明为 CSOM_Net_Base 的子类, 所以它拥有所有子类的函数, 加上它自己的函数。创建该类作为加载范形解析器函数的前奏。利用解析器获取的范形可以进一步用于加载训练范形和范形识别。
包含范形的文件名称在参数里设置:
input string DataFileName="SOM\\optim.csv";
请记住 - 文件名要带扩展名, 即为文件的全名。解析文件利用 FILE_CSV 标志打开。
然后, 调用与前例中相同的函数: 网络加载, 文件加载, 文件解析, 在循环里识别范形并打印。
在此我们应考虑解析器设置的范形文件格式。
文件首行应是列名 标题。这是一个强制性条件。如果文件的标头未填写, 则解析器将从首行切除以上的样本, 并创建列名。
文件中每一列用于存储相同深度的范形数据。相应地, 每一行单独存储范形。换言之, 每个向量位于水平, 而所有向量的相同字段 - 位于垂直。
解析器将范形数据放置于内置的 m_patterns_sets_array 数组, 并识别字段头部, 将它们存储在 m_som_titles 数组 (在子类中声明), 以及以行号填写 m_patterns_titles 数组。据此, 根据其行号在文件中查找所需范形没有任何问题。
第三个示例 Sample3_SOM_Net_Train 是最迷人的。首先, 它已有 CSOM_Net_Train 连接, 其为第二个基本类且该类用于训练 Kohonen 映射。第二, 它是最后一个能够无需观察自动训练的类, 随后的类仅连接图形外壳。因此, 在 "Kohonen 映射原理" 一节中描述的所有内容在前三个类中均已实现。之后, 类有继承分支。
对于后续类, 计算和绘制 bmp 图像的函数必须在网络训练函数 Train() 里被调用。但由于第三个样品没有图表, 在此不需要这些函数的实体。为了解决这个冲突, Render() 和 ShowBMP(bool back) 函数被声明为 virtual, 实体为空, 而所需的代码在 CSOM_Net_Img 子类里定义。
现在,我们将直接转移到性训练。参数应在训练前发送到网络。为此目的, 有一个服务函数 InitParameters(int iterations,int xcells,int ycells) 此处传递的参数: 训练迭代次数 — iterations 并且网络大小切分为两个参数 — xcells, ycells (分别为 X 和 Y 大小)。
训练循环是在 Train() 函数里分配。它需要在调用训练循环之前初始化网络。这意味着初始化类的均匀分布数值, 定义数组大小, 计算使用范形的最大最小列, 在设置的随机数据范围内初始化节点。
为了更好地理解代码, 并最终能轻松地访问, 实际的训练迭代被转化为一个专门的单独函数 TrainIterations(int &p_iter)。这样编写实现, 是为了让其他的程序员能理解精髓, 并在必要时能够自行修改。函数包含的算法在 "Kohonen 映射原理" 一节中已经描述, 因而在此无需赘言。
通常, 第三个示例展示了从 CSOM_Net_Train 类连接网络的一致性: 加载带有训练范形的文件, 发送参数, 调用训练, 在文件里保存网络。
当说明读取网络文件的函数时, 上文已提及, 函数的保存和读取应同步考虑, 以便理解格式。
void CSOM_Net_Train::SaveNet(string file_name) bool CSOM_Net_Base::DownloadNet(string file_name)
双精度 类型普遍存在于网络数据中。所以, 网络保存于 双精度 类型的一维数组中。数据的其余部分均被转换为这种类型。
首先, 头部保存于网络。保存的前五个 双精度 字段是有关网络的必要数据。
然后, 执行节点网络拷贝。首先, 节点坐标的 6 个基准, 然后 - 节点的权重。所有数据均按此方式逐个复制。
从头至尾, 按照 标题 枚举将 双精度 格式的二进制数添加到网络。
所以, 这就是我们得到的。我们从头部知道范形尺寸和节点的数量。此数据可用于计算节点从何处开始和结束的。保存的 标题 网络从节点的终点到文件的结尾。它们被放置在文件的结尾, 因为预测所需内存的大小是不可能的。
第四个示例 Sample4_SOM_Net_Img 揭示图形外壳的连接。在以前的文章里, 必须连接到由 Dmitry Fedoseev 编写的 cintbmp.mqh 图形库以便调用它。但是, 我不得不将之修改, 删除所有 WinAPI 的 DLL 调用。这样就可以在 市场 里使用代码。更新后的文件包含有关作者信息, 但我已将之改名为 cintbmp2.mqh。
在代码中仅修改了用来保存/加载 bmp 文件至 Image 目录的函数。现在, 文件记录只用于保存, 但不用于显示。为了显示目的, 它们即刻加载到资源, 这有助于避免调用 DLL 库。
调用函数的顺序类似于前面的示例。尽管现在已连接了图形外壳, 依然需要额外的设置参数以便其操作。所以, 之前子类中声明的初始函数重新启动, 以适合图形外壳的新参数。
void InitParameters(int iterations,int xcells,int ycells, int bmpwidth,int bmpheight, bool p_HexagonalCell,bool p_ShowBorders,bool p_ShowTitles, int p_ColorScheme,int p_MaxPictures);
参数通过函数接口传递, 以防止在类代码中进行变量的全局声明。否则, 它将导致连接项目的处理复杂化, 因为其中的变量名可能不同。
接着是从示例 CSOM_Net_Train 类里调用训练函数 Train()。但是由于在函数实体里有操纵图表的代码 Render() 和 ShowBMP(bool back), 它将在每百次迭代时显示训练进程。当从 Train() 退出后, 调用这些函数来显示最后的变化。
第五个示例 Sample5_SOM_Net_Player 显示扩展类的例子。它并非网络的基础, 但它展示了现有的代码如何轻易地进行审查。为此目的, 它声明为一个基本类的子类就足够了。
为什么要编写子类?我们的类可以从各种可用的类继承, 利用 protected 命令可以隐藏所有类中的所有函数。例如, 当作为 CSOM_Net_Img 类的子类连接时, 我们将得以访问之前类中所有声明为 protected 和 public 的函数和数据。以这种方式, 程序可以根据您的需要来配置, 这取决于需要获得什么。
因此, CSOM_Net_ Player 类的扩展是一个图形面板, 用来控制 Kohonen 网络。为了编写它, 我已经连接了带有图形库 Dmitry Fedoseev 的 IncGUI_v3.mqh 文件, 之后略作修改。改进会影响图形标签 (并非边缘, 而在对象的顶部) 以及标签颜色的显示。我尚未对原始文件进行任何修改, 我以声明库类的继承来替代。
即使创建图形面板占据了大部分的代码, 但它不会带来任何困难。这是编写代码的常规工作。我打算进一步阐述 Kohonen 神经网络图形面板和图形外壳之间的协调。
首先, 必须指出的是, 启动网络的方法包括两部分。第一部分执行训练并保存已训练的网络到二进制文件 somnet。第二部分从文件加载, 并在加载的范形基础上搜索适合的节点。每一部分均是独立的, 虽然它们使用相同的字段显示数据: 范形文件的前进路径输入字段, 神经网络文件的前进路径的输入字段。
然而, 模式是真正独立的。训练范形并非强制性的取自同一文件的范形用于识别。这同样适用于神经网络。您可以在一个文件里训练网络, 然后将模式切换到 "操作" 并加载其它网络用于识别。
重要提示: 切换模式会将模式重置为零状态。
所以, 模式是独立的。但是, 按钮的状态改变时程序的快速响应应如何实现?这个问题就是程序不使用循环来训练或搜索范形。它是基于事件分配。
当程序进入训练模式, 它会初始化, 加载参数, 准备内存, 和首次迭代, 之后给其自身发送一个低于数字 333 的用户事件。进入事件处理程序是通过在对象上点击鼠标进行过滤, 结束对象编辑和用户事件 333。
依此方式, 如果没有中断 (如果 "开始" 和 "训练" 按钮的状态没有改变, 我们仍然可访问训练入口), 新的训练迭代发生, 且一个新的 333 消息被发送。依此类推, 直到网络训练完毕, 并停止循环, 或用户使用按钮停止训练。
在范形搜索的模式里实现了同样的中断方案。
我曾尝试尽可能令面板界面简洁。主要是来自以前示例是哦那个的重复 input 变量。我们仍然会显示一些按钮和输入字段的值:
启动面板需要:
四十年前, 神经网络是科学思潮的领导前端。大约 20 年前, 唯有熟悉神经网络算法的人士, 才会被认定是一个专家。现在, 术语 "神经网络" 似乎不能唬住任何人。模糊逻辑算法, 神经网络现已深深地隐藏在交易中, 显然, 它并没有那么复杂。我希望本文能够有助于启发您对 Kohonen 映射的灵光, 并轮流使用它们工作。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程