神经网络实践:熟能生巧
概述
大家好,欢迎阅读又一篇关于神经网络的文章。
在上一篇文章“神经网络实践:第一个神经元”中,我们构建了第一个神经元。然而,编程并非那么简单。许多人只是简单地创建,或者更确切地说是复制,一些代码并开始使用。这绝对不是问题。这完全没有任何问题;恰恰相反。这是一种相当明智的做法,当然,前提是我们需要研究代码并尝试对其进行改进,以满足我们的需求。错误的做法是在不了解其工作原理的情况下使用某样东西。或者更糟的是,抱怨某事,或者开始谈论我们完全不了解的事情。
尽管能正常运行,但我们在上一篇文章中研究的代码在任何情况下都不应被用于更复杂的应用程序中。这样做是为了确保所有事情都能及时完成,或者至少达到预期的结果。
为了让亲爱的读者们理解我在说什么,让我们来看看实际情况,以及如果我们做出一些改变,可能会出现哪些问题。但为了更清晰地表达,让我们将其视为一个新的话题。
执行时间变得不合理地漫长
太好了,我们简单而独特的神经元可以在以下代码中看到。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRandom (rand() / (double)SHORT_MAX) 05. #define macroSigmoid(a) (1.0 / (1 + MathExp(-a))) 06. //+------------------------------------------------------------------+ 07. double Train[][3] { 08. {0, 0, 0}, 09. {0, 1, 0}, 10. {1, 0, 0}, 11. {1, 1, 1}, 12. }; 13. //+------------------------------------------------------------------+ 14. const uint nTrain = Train.Size() / 3; 15. const double eps = 1e-3; 16. //+------------------------------------------------------------------+ 17. double Cost(const double w0, const double w1, const double b) 18. { 19. double err; 20. 21. err = 0; 22. for (uint c = 0; c < nTrain; c++) 23. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 24. 25. return err / nTrain; 26. } 27. //+------------------------------------------------------------------+ 28. void OnStart() 29. { 30. double w0, w1, err, ew0, ew1, eb, bias; 31. ulong count, it0, it1; 32. 33. Print("The Neuron - Tutor ..."); 34. MathSrand(512); 35. w0 = (double)macroRandom; 36. w1 = (double)macroRandom; 37. bias = (double)macroRandom; 38. 39. it0 = GetTickCount(); 40. 41. for (count = 0; (count < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); count++) 42. { 43. ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; 44. ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; 45. eb = (Cost(w0, w1, bias + eps) - err) / eps; 46. w0 -= (ew0 * eps); 47. w1 -= (ew1 * eps); 48. bias -= (eb * eps); 49. } 50. 51. it1 = GetTickCount(); 52. Print("Time: ", (it1 - it0) / 1000.0, " seconds."); 53. PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", count, w0, ew0, w1, ew1, bias, eb, err); 54. Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); 55. Print("Error Weight 0: ", ew0); 56. Print("Error Weight 1: ", ew1); 57. Print("Error Bias: ", eb); 58. Print("Error: ", err); 59. 60. Print("Testing the neuron..."); 61. for (uchar p0 = 0; p0 < 2; p0++) 62. for (uchar p1 = 0; p1 < 2; p1++) 63. PrintFormat("%d AND %d IS %f", p0, p1, macroSigmoid((p0 * w0) + (p1 * w1) + bias)); 64. 65. Print("************************************"); 66. } 67. //+------------------------------------------------------------------+
和上一篇文章里给出的代码相比,我们做了一些修改。原因很简单,我们想展示一个简单的改变是如何显著影响整体执行时间以及预期结果的。当上述代码在 MetaTrader 5 中运行时,会生成与此非常相似的结果:

请注意上图中显示的一些数据。最显而易见的一点,也是我们想要强调的一点,当然是代码执行时间。这个时间仅反映了我们调整神经元参数的部分。您可以通过查看代码来了解这一点。请注意,在第 39 行,我们捕捉到了神经元训练过程开始前的精确时刻。在第 51 行,我们再次捕捉到了同样的时刻,即训练结束后的时刻。这两个值之间的差值就是训练执行时间。请注意,第 52 行我们进行了一个小计算。由于捕获的时间是以毫秒为单位测量的,因此将时间值之间的差值除以一千,即可得到近似秒数。这正是上图中标出的那段时间。
现在让我们看看我们要做什么。在同一代码的第 15 行,我们将误差设置为 0.001。但是,如果我们把误差改为 0.0001,即 1e-4,那么这段代码的执行时间会发生什么变化呢?
好的,请在你自己的电脑上试试,看看会发生什么。总之,你会得到类似这样的结果:

现在请注意:仅仅将误差从 1e-3 改为 1e-4,执行时间就会从不到 1 秒增加到接近 80 秒。这正是你所看到的。精度仅仅十倍的变化,就导致执行时间增加了 80 倍以上。
正是这样的时刻最终击碎了众多初学者的梦想,尤其是那些不知道编程绝非易事的人。当我们说每个程序通常都是为了执行特定任务而设计时,许多人会感到惊讶,尤其是当有人声称神经网络可以非常迅速地完成任何任务时,尽管事实上这并不完全正确。
亲爱的读者,我想提醒你们注意这一事实。在继续向网络中添加新神经元之前,了解网络的构建方式和用途是极其重要的。为了证明这一点,我们将使用完全相同的简单神经元,并用略有不同的东西来替换它,同时仍然旨在解决与代码中相同的问题。换句话说,我们将使神经元更加专业化 — 不是在拟合能力这个层面上,而是从其设计处理的输入数量方面。为了区分这些话题,我们单独用一节来讨论这个问题。
调整代码以获得更好的结果
现在让我们思考一下:仅仅为了将精度提高 10 倍,就让单个神经元的运行时间延长 80 倍,这有什么意义呢?这种事毫无道理。然而,如果我们事先知道神经元在其整个生命周期中都将处理两个输入和一个输出,那么我们就可以改进它的代码。这将加快训练速度,并使神经元在执行这一特定任务时更加高效。
做出这些改变可能看起来非常困难,仿佛只有知识渊博的人才能完成这样的壮举。然而,事实并非如此。亲爱的读者们,你们只需要了解该程序的工作原理及其目的即可。如果你们了解这一点,并且明白程序是如何用特定语言编写的,那么你们就有能力(尽管可能需要一些时间)实现一个更好的解决方案。
为了证明这一点,我们将在保留原始代码的同时,对上一主题中的代码进行修改。我们的目标是了解我们是否正朝着正确的方向前进。优秀的程序员绝不会尝试修改未知代码。首先,他们会尝试理解代码,弄清楚它的工作原理,然后尝试进行一些改进。考虑到以上因素,新的代码如下所示:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRandom (rand() / (double)SHORT_MAX) 05. #define macroSigmoid(a) (1.0 / (1 + MathExp(-a))) 06. //+------------------------------------------------------------------+ 07. #define def_Fast 08. //+------------------------------------------------------------------+ 09. double Train[][3] { 10. {0, 0, 0}, 11. {0, 1, 0}, 12. {1, 0, 0}, 13. {1, 1, 1}, 14. }; 15. //+------------------------------------------------------------------+ 16. const uint nTrain = Train.Size() / 3; 17. const double eps = 1e-4; 18. //+------------------------------------------------------------------+ 19. double Cost(const double w0, const double w1, const double b) 20. { 21. double err; 22. 23. err = 0; 24. for (uint c = 0; c < nTrain; c++) 25. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 26. 27. return err / nTrain; 28. } 29. //+------------------------------------------------------------------+ 30. double Cost_2(double &w0, double &w1, double &b) 31. { 32. double err, ew0, ew1, eb; 33. 34. err = ew0 = ew1 = eb = 0; 35. for (uint c = 0; c < nTrain; c++) 36. { 37. err += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 38. ew0 += MathPow((macroSigmoid((Train[c][0] * (w0 + eps)) + (Train[c][1] * w1) + b) - Train[c][2]), 2); 39. ew1 += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * (w1 + eps)) + b) - Train[c][2]), 2); 40. eb += MathPow((macroSigmoid((Train[c][0] * w0) + (Train[c][1] * w1) + (b + eps)) - Train[c][2]), 2); 41. } 42. 43. w0 -= (((ew0 - err)/ eps) * eps); 44. w1 -= (((ew1 - err)/ eps) * eps); 45. b -= (((eb - err)/ eps) * eps); 46. 47. return err / nTrain; 48. } 49. //+------------------------------------------------------------------+ 50. void OnStart() 51. { 52. double w0, w1, err, ew0, ew1, eb, bias; 53. ulong count, it0, it1; 54. 55. Print("The Neuron - Tutor..."); 56. MathSrand(512); 57. w0 = (double)macroRandom; 58. w1 = (double)macroRandom; 59. bias = (double)macroRandom; 60. 61. it0 = GetTickCount(); 62. #ifdef def_Fast 63. for (count = 0; (count < ULONG_MAX) && ((err = Cost_2(w0, w1, bias)) > eps); count++); 64. #else 65. for (count = 0; (count < ULONG_MAX) && ((err = Cost(w0, w1, bias)) > eps); count++) 66. { 67. ew0 = (Cost(w0 + eps, w1, bias) - err) / eps; 68. ew1 = (Cost(w0, w1 + eps, bias) - err) / eps; 69. eb = (Cost(w0, w1, bias + eps) - err) / eps; 70. w0 -= (ew0 * eps); 71. w1 -= (ew1 * eps); 72. bias -= (eb * eps); 73. } 74. #endif 75. it1 = GetTickCount(); 76. Print("Time: ", (it1 - it0) / 1000.0, " seconds."); 77. PrintFormat("%I64u > w0: %.4f %.4f || w1: %.4f %.4f || b: %.4f %.4f || %.4f", count, w0, ew0, w1, ew1, bias, eb, err); 78. Print("w0 = ", w0, " || w1 = ", w1, " || Bias = ", bias); 79. Print("Error Weight 0: ", ew0); 80. Print("Error Weight 1: ", ew1); 81. Print("Error Bias: ", eb); 82. Print("Error: ", err); 83. 84. Print("Testing the neuron..."); 85. for (uchar p0 = 0; p0 < 2; p0++) 86. for (uchar p1 = 0; p1 < 2; p1++) 87. PrintFormat("%d AND %d IS %f", p0, p1, macroSigmoid((p0 * w0) + (p1 * w1) + bias)); 88. 89. Print("************************************"); 90. } 91. //+------------------------------------------------------------------+
不用担心编写这段代码。如果你愿意,你可以这样做,以便更好地理解如何用 MQL5 编程,因为当我们编写代码时,我们就能更好地理解它是如何创建的。此外,我们还解决了与编程语言语法相关的其他各种问题。但你可以在附件中查看。所以,如何运用这些知识取决于你自己。
现在,让我们看看在执行时间方面会得到什么结果。如下图所示。

“什么?这怎么可能?时间怎么会从将近 80 秒一下子降到 18 秒多一点?这绝对不可能。啊,我知道了,你换了台电脑,用的是另一台性能更强大的电脑。或者,几乎可以肯定的是,你通过某种方式作弊,比如在系统中引入另一个错误或更改其他内容。这样一来,如第一张图所示,第一次运行所需时间会更长,但之后通过使用一些技巧,你将节省大量时间。这样就能得到图中第二张图片。”
亲爱的读者,或许我们本可以这样做。但是,不,这正是我们为何将代码留在附件中的原因。通过在您自己的电脑上尝试相同的代码,您将能够理解发生了什么。但在尝试之前,让我们先来分析一下,为何之前需要近 80 秒的代码现在仅需 18 秒多一点就能运行。首先,我要说的是,这里并没有什么魔法或类似的东西。关键在于理解神经元必须学习什么。
如果你查看这段代码,可能会觉得它非常复杂,尤其是如果你刚开始编程的话。不过,别担心;它其实很简单。我确保它一直保持简单。这样,对其工作原理的解释就会更直观、更便于理解。
让我们从第 07 行开始。这一行包含了一个定义,而这是作为初学者的你,少数几个必须修改代码而不会使其变得不稳定的地方之一。如果删除第 07 行,代码将与上一节中的完全相同。也就是说,它依然和前一节一样:速度慢、效率低,但胜在通用。然而,如果按照本文中的示例定义第 07 行的宏,则将使用代码的快速版本,你会看到如上图所示的结果。
现在,我想让你将本主题中的图像与上一个主题中的图像进行比较,同时注意误差值。请注意它们有多么相似。尤其是上一个主题中的最后一张图像和这里展示的这张图像。换言之,在这两种情况下,我们使用的误差都是 1e-4。“等一下。你是说尽管使用了相同的精度,执行时间却急剧下降?”是的,亲爱的读者们,这是有可能的。这正是发生的情况。“但是为什么会这样呢?你能给我解释一下吗?”
好的,为了理解这一点,我们需要查看代码中的几个关键点。让我们跳过大部分代码,直接来看位于第 50 行的 OnStart 过程。现在我们需要一点耐心,但理解其中的运作原理其实并不难。请注意,此过程中的大部分代码已在前一节中讨论过。这些相同的部分使得这个神经元与我们之前看到的神经元相同。但是,在第 62 行,我们要求编译器在创建最终可执行文件之前执行检查。这项测试检查第 07 行的定义是否存在。
如果它不存在,生成的代码将与上一主题中讨论的代码相同。如果第 07 行的宏被定义,则上一主题中的代码将被第 63 行的代码替换。请注意这一点。我们将第 65 行到第 73 行之间的代码替换为第 63 行的代码。其他内容保持不变,未做任何修改。
但现在,我们来看第 63 行的内容,可以看到它是一个 for 循环。它与第 65 行的代码非常相似。当然,区别在于第 65 行我们调用 Cost,而第 63 行我们调用 Cost_2。在其他所有方面,代码都是相同的。“但这并不能解释为什么一种情况下需要将近 80 秒,而另一种情况下只需要 18 秒。这种事完全没有道理。”但亲爱的读者,这正是你出错的地方。这个简单的改变可能会带来截然不同的结果。请注意,虽然我们只看了第 65 行,但实际上 Cost 函数被调用了四次:分别在第 65 行、第 67 行、第 68 行和第 69 行。
对于每一次这样的调用,都必须分析整个表、数据库,或者在我们的例子中,是训练数组。请思考片刻。注意,我们的训练数组只包含四行数据。这可以从代码的第 09 行看出。只有四行数据。然而,每次需要调用位于第 19 行的 Cost 函数时,第 24 行的循环都会再次读取并计算所有内容。这适用于 OnStart 过程中出现的所有调用。
这种编程风格大大简化了任务,但通常效率不高。然而,正是这种编程风格使神经元具有了更强的通用性。这是因为神经元可以根据需要处理任意数量的输入或输出,而无需更改代码的相应部分。这正是了解我们在做什么,或者了解预期或想要的结果变得至关重要的地方。许多人会从第 19 行提取这个函数,并将其转换为 OpenCL 代码,甚至转换为可在并行系统上运行的代码。在某些情况下,运行并行应用程序的机器可能相当昂贵。
在其他情况下,即使是一块好的显卡也能满足需求。然而,我们只是简单地用一种东西替换另一种,因为代码仍然效率低下。我们之所以这么说,是因为每次计算成本时,我们都必须扫描用于训练的整个数据库。想象一下一个包含数千兆字节数据的数据库。
现在让我们回到第 63 行。乍一看,第 63 行似乎没什么特别的,但它调用了一个专用函数。与非专用执行相比,这大大加快了执行速度。然而,拥有一个专用神经元会使编程变得稍微复杂一些,因为如果我们需要更改神经元中的某些内容,无论是输入数量还是输出数量,我们都必须完全更改 Cost_2 函数的代码,而该函数在第 63 行的调用频率较低。原因恰恰在于其操作方式。让我们来看看这是如何工作的,然后再来看第 30 行,那里实现了 Cost_2 函数。但是为了让解释更清晰,让我们把这个函数作为一个新主题来研究。
单个神经元的功能特化
现在,对于那些希望更冷静地研究一切并从本质上理解其工作原理的人来说,让我们将 Cost_2 函数分为两部分。这样将更容易解释它为何比标准版本运行得更快。
好的,第一部分在第 35 行到第 41 行之间。这些代码行的作用是什么?它的作用与第 65、67、68 和 69 行以前的作用相同。当然,除此之外,它还执行与第 24 行中的 for 循环相同的操作。换句话说,我们最终会执行第 25 行。然而,与之前不同的是,我们现在同时执行所有操作。因此,我们需要比之前更多的变量。第 32 行中声明的每个变量都有助于加快代码执行速度。在第 37 行,我们计算神经元的总误差。这并不会加快处理速度,但在第 38、39 和 40 行,我们计算了先前由第 65 行开始的那段循环中得到的其他误差项。
现在我们来谈谈问题的难点。第 40 行是每个神经元的一部分。换句话说,它与第 37 行一样,是通用代码。但第 38 行和第 39 行则不是。它们正是使神经元具有特异性的部分。换句话说,如果我们需要为神经元添加更多输入,我们就必须在代码中添加更多这样的行,而为某一特定目的创建的神经元很可能对其他任何目的都无用。然而,这种方法使得代码的运行速度远快于针对特定任务优化不佳的更通用实现。
代码的第二部分在第 43、44 和 45 行。在那里,我们更正了成本函数下一次调用的值。此前,这个操作是在第 70、71 和 72 行完成的。
只需对代码进行这些小小的修改,就能让神经元运行得更快,使其能够学习,或者更准确地说,是生成一组值作为存储的先验知识。这才是这种编程方式的真正魅力所在。它不在于调用本身作为测试,而在于意识到一切都可以更高效地完成的乐趣。请注意,甚至不需要使用 OpenCL 或编写并行化代码。这比许多人表现出的枯燥乏味要有趣得多,尤其是在讨论神经网络的话题时。
好的,这是一个相当简单的例子,展示了如何实现这一点,但我们还可以就同一个问题更深入地探讨。尽管这个简单的神经元能够做很多事情,其唯一的限制是需要不断增减输入的数量,当然,同时总是试图尽可能快地做到这一点,但它仍然不够“智能”,无法处理某些特定情况。因此,人工智能或神经网络并不像许多人认为的那么简单。同样,也不能假定神经元能够处理特定场景中的任何情况。
为了说明这一点,让我们进行一个小小的思维实验。这里并没有什么复杂的内容;我只是想向你们展示一些在研究神经网络时许多人会忽略的东西。许多人对此保持沉默,或者不想谈论它,但如果你理解了,你就会意识到,没有任何程序能超越创造它的心智。
让我们来看一下用于训练的数据。有些情况下,神经元无法处理这些数据。这并不是因为数据极其复杂或类似的原因;本质上,数据很简单。但是,单个神经元无法解决问题,我重复一遍,它无法解决问题,因此必须使用多个神经元。因此,我们将简要概述一下稍后将要讨论的主题。然而,在某些特定情况下,用单个神经元来模拟一个小型神经网络也是可行的。这完全取决于我们要寻找解决方案的任务类型。
因此,在许多情况下,实际上在大多数情况下,一个神经元都可以被构建 — 更准确地说,被编程 — 以便将来作为实现神经网络的组成单元来使用。我所说的并非像聊天机器人或类似的东西那样的神经网络;我说的是更简单的东西。然而,在某些情况下,我们可以让单个神经元解决通常需要小型神经网络才能完成的任务。而且,我们不需要用大量数据来创建大数据来找到这种情况。使用本文中展示的相同代码就可以做到这一点。
“哇,现在一切都变得复杂多了。为什么?让我看看我是否理解正确。你是说,我们正在研究的这个神经元无法处理可以在这个相同数据库中表示的东西,但如果我们知道如何处理它,我们就可以让它模仿一个小型神经网络,从而解决它自己无法完成的任务?是这样吗?”是的,没错。请注意,我们正在使用的训练数据集类似于一个逻辑门真值表。换句话说,我们可以向其输入由 0 和 1 组成的组合,并要求神经元尝试找到一个方程,或者更准确地说,是一个能够表示逻辑门的线性函数。
然而,存在两种情况,尽管我们可以将它们视为一种情况。这是因为它们互为反函数式的逻辑对立项。在这些情况下,这个简单的神经元将无法找到正确的方程,但如果我们强制它以某种方式工作,就有可能找到正确的方程。
为了让我想解释的内容更容易理解,请看下面的图片。

此图显示了这里考虑的主要逻辑门。绿色部分包含门输入。输出以红色和橙色调显示,门类型在每个图表的顶部标明。
现在让我们来看看这意味着什么。从左到右,这些门按以下顺序排列:与门(AND)、与非门(NAND)、或门(OR)、或非门(NOR)、异或门(XOR)和异或非门(NXOR)。
如果我们的主题是神经网络,那么图像中的这张表对我们有何意义呢?为了理解这一点,让我们再次查看神经元代码。请注意,在第 09 行,我们定义训练规则的地方,有一些内容与图像中显示的表格完全相同。
现在到了有趣的部分:前两个值是输入数据。而第三个值则是结果。将你在代码中看到的内容与图片中显示的表格进行比较。你注意到了什么?嗯,如果你看得更仔细一些,你会发现我们在训练过程中定义了与门。
你可以通过改变训练中使用的第三个参数的值来进行实验。你会发现,你可以让神经元学习或生成一个能够表示几乎所有门的方程。我说“几乎”是因为神经元将无法学习其中的两个门,即异或(XOR)及其逆运算(NXOR)。
“哇!但请等一等。为什么神经元不能学会生成 XOR 和 NXOR 门?你在开玩笑吗?所有的值都只有 0 和 1。这只能是个玩笑或者恶作剧。你在取笑我吗?”
我甚至想跟你开个玩笑,但事实上并非如此。这是事实,你可以去验证。如果你只用 0 和 1 来训练单个神经元,那么在某些情况下,神经元将无法学会如何处理这种情况。这是因为解决方案比单个神经元所能建模的要复杂得多。
你不太可能听到有人谈论这些事情,甚至有人会避而不谈。这种情况通常发生在我们正在研究的话题与人工智能、神经网络或神经元编程相关时。人们不会考虑尝试所有替代方案。如果某件事在特定情况下有效,他们会立即开始传播这一信息。但如果你问他们具体的情况,他们就会开始闪烁其词或忽略问题。
然而,有一种方法可以训练神经元来处理这种情况,例如当我们需要解决 XOR 门问题或其逆问题,即 NXOR 门时。由于这个话题在本文范围内很难解释清楚,在下一篇文章中,我们将探讨如何解决这个问题。但在下一篇文章发表之前,亲爱的读者们,也许你们可以自己想出一个解决 XOR 门及其逆问题,即 NXOR 门的方法。注意:不能使用多个神经元;那将是作弊。规则是:只能使用一个神经元来实现解决方案。
最后的想法
在本文中,我们展示了如何通过针对神经元进行专门化的小幅代码更改来显著加快训练阶段。因为一旦神经元或神经网络(我们稍后会看到)经过训练,其执行工作就会变得非常快。以至于在本文本身和附件中提供的代码中,你可以看到,从输入数据生成结果的速度非常快。当然,这是在我们已经知道应该在计算中使用哪些值的前提下。
在此,我们还提到了一个几乎无人谈论甚至无人提及的现存问题。恰恰是,与许多人习惯认为的相反,神经元并不能处理所有可能的情况。机器并没有那么完美,也不可能超越生命体。单个生物神经元能够解决任何简单任务。然而,编程后的神经元或硅基实现的神经元却无法完成这样的壮举。
所以,在下一篇文章中,我们将继续探讨这个问题。再见,期待尽快再会。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/13748
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从新手到专家:用MQL5实现动态新闻头条(八)—— 新闻交易快捷交易按钮
新手在交易中的10个基本错误
您应该了解的MQL5向导技巧(第七十八部分):鳄鱼振荡器与AD指标的市场韧性策略