ニューラルネットワークの実践:習うより慣れよ
はじめに
こんにちは。ニューラルネットワークに関する新たな記事へようこそ。
前回の「ニューラルネットワークの実践:最初のニューロン」では、初めてのニューロンを作成しました。しかし、プログラミングはそれほど単純ではありません。多くの人は、単純にコードを作成したり、あるいは既存のコードをコピーしたりして、そのまま利用し始めます。これは決して問題ではありません。むしろ、まったく悪いことではありません。もちろん、そのコードを理解し、自分たちの目的に合うように改善しようとする姿勢が伴っているのであれば、これは非常に合理的な方法です。問題なのは、仕組みを理解しないまま何かを利用することです。さらに悪いのは、理解していないものについて不満を述べたり、まったく知識のない事柄について議論を始めたりすることです。
前回の記事で扱ったコードは、機能的には動作します。しかし、より複雑なアプリケーションで使用することは決して推奨されません。これは、すべての処理が適切な時間内、あるいは少なくとも期待される範囲内で完了するようにするために必要です。
読者の皆さんに、私が何を指摘しているのかを理解していただくために、いくつかの変更を加えた場合、どのような状態になるのか、そしてどのような問題が発生する可能性があるのかを確認してみましょう。ただし、より分かりやすくするために、今回は新しいテーマとして見ていきます。
実行時間を不合理なほど長くする
さて、私たちのシンプルで独自のニューロンは、以下のコードで確認できます。
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行目では、同じように学習処理が完了した直後の時刻を取得しています。この2つの値の差が、学習処理の実行時間になります。なお、52行目では小さな計算を行っています。取得された時刻はミリ秒単位であるため、2つの時間値の差を1000で割ることで、おおよその秒単位の時間へ変換しています。これが、上の画像で示されている時間になります。
では、ここで何おこなうのかを見てみましょう。同じコードの15行目では、誤差値を0.001に設定しています。では、この誤差値を0.0001 (1e-4)に変更した場合、同じコードの実行時間はどのように変化するでしょうか。
ぜひ、ご自身のコンピュータで試してみてください。いずれの場合でも、非常によく似た結果が得られるはずです。

ここで重要なのは、単純に誤差値を1e-3から1e-4に変更しただけで、実行時間が1秒未満から約80秒まで増加している点です。つまり、精度を10倍高めただけで、実行時間は80倍以上に増加しました。
まさに、このような場面が、多くの初心者の夢を打ち砕く瞬間になります。特に、プログラミングが決して簡単な作業ではないことを理解していない人にとっては、大きな驚きとなります。「それぞれのプログラムは、多くの場合、特定の目的を達成するために設計されている」と説明すると、多くの人は驚きます。特に、「ニューラルネットワークは何でも高速に処理できる」と聞いた後ではなおさらです。しかし、実際には必ずしもそうではありません。
読者の皆さんには、この点をぜひ意識していただきたいと思います。ネットワークへ新しいニューロンを追加する前に、そのネットワークをどのように構築するのか、そして何を目的として使用するのかを理解することが非常に重要です。これを説明するために、先ほどの非常に単純なニューロンを取り上げ、コード内で解決しようとしていた問題自体は変えずに、少し異なる構造へ置き換えてみます。つまり、ニューロンを少しだけ特化させます。ただし、当てはまり方や近似性能の意味での特化ではなく、処理する入力数を限定し、その用途に合わせて設計するという意味での特化です。これらのテーマを混同しないために、続きは別のセクションで詳しく説明します。
より良い結果を得るためのコード調整
ここで、次の点について考えてみましょう。たった1つのニューロンの精度を10倍向上させるために、処理時間が80倍も長くなるというのは、一体どのような意味があるのでしょうか。このような設計には合理性がありません。しかし、もし事前に、運用中つねに2つの入力と1つの出力を扱うと分かっているなら、コードを改善することができます。その結果、学習処理を高速化し、この特定のタスクに対してニューロンをより効率的に学習させることが可能になります。
このような変更を加えることは、一見すると非常に難しく、高度な知識を持った専門家だけが実現できるように思えるかもしれません。しかし、実際にはそうではありません。読者の皆さんに必要なのは、プログラムがどのように動作しているのか、そして何を目的としているのかを理解することだけです。これらを理解し、さらに特定のプログラミング言語でどのようにコードが記述されているのかを把握できれば、多少時間はかかるとしても、より良い解決策を実装できるようになります。
これを実証するために、前のテーマで使用したコードを変更します。ただし、元のコードはそのまま残します。目的は、私たちが進んでいる方向が正しいのかどうかを確認することです。優れたプログラマーは、理解していないコードをいきなり変更しようとはしません。まずコードを理解し、その仕組みを把握し、その後で改善を試みます。こうした考え方を踏まえて、新しいコードを以下に示します。
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秒程度まで短縮できるのでしょうか。そんなことは絶対に不可能です。ああ、分かりました。コンピュータを変更して、より高性能なものを使ったのですね。あるいは、おそらく何らかの方法でシステムに別のエラーを入れたり、何か別の部分を変更したりして、ごまかしたのでしょう。そうすれば、最初の実行では最初の画像のように時間がかかりますが、その後は何らかの工夫によって大幅に短縮できます。その結果、2枚目の画像のような結果を得られるわけですね。」
さて、読者の皆さん。確かに、そのようなことをすることもできたかもしれません。しかし、実際にはそうではありません。だからこそ、コードを添付しています。同じコードを皆さん自身のコンピュータで試してみれば、何が起こったのか理解できるでしょう。ただし、その前に、なぜ以前は約80秒かかっていたコードが、今回は18秒程度で実行できるようになったのかを確認してみましょう。まず最初に言っておきます。ここには魔法のようなものは何もありません。すべては、ニューロンが何を学習する必要があるのかを理解することに関係しています。
コードを見ると、特にプログラミングを始めたばかりの方には、非常に複雑に見えるかもしれません。しかし、心配する必要はありません。実際には非常に単純です。私は、この仕組みができるだけ分かりやすくなるように設計しました。そのため、動作原理も、より視覚的かつ実践的に説明することができます。
まず、7行目から始めましょう。ここには定義(define)が記述されています。そして、ここは初心者の皆さんがコードを変更しても、プログラムを不安定にするリスクが比較的少ない数少ない場所の1つです。7行目を削除した場合、コードは前のセクションで説明したものと完全に同じになります。つまり、低速で非効率ではありますが、依然として汎用的なコードになります。しかし、この記事で示しているように7行目のマクロを定義すると、高速版のコードが使用され、上の画像に示された結果が得られます。
ここで、このテーマの画像と、前のテーマの画像を比較してみてください。特に誤差値に注目してください。非常によく似ていることが分かると思います。特に、前回の記事の最後の画像と今回の画像は非常によく似ています。つまり、どちらの場合も誤差値として1e-4を使用しています。「待ってください。つまり、同じ精度を使用しているにもかかわらず、実行時間が大幅に短縮されたということですか。」はい、読者の皆さん。それは可能です。実際にそれが起こりました。「しかし、なぜそのようなことが起こったのですか。説明してもらえますか。」
もちろんです。これを理解するには、コード内のいくつかのポイントを見る必要があります。コードの大部分は一旦省略し、50行目にあるOnStart手続きへ進みましょう。ここでは少しだけ我慢が必要ですが、起こっていることを理解するのは決して難しくありません。この手続き内のコードの大部分は、すでに前のセクションで説明しています。これらの共通部分こそが、今回のニューロンを以前見たものと同じ構造にしています。しかし、62行目で、コンパイラに対して最終的な実行ファイルを作成する前にチェックをおこなうよう指示しています。このチェックでは、7行目の定義が存在するかどうかを確認します。
もし存在しない場合、生成されるコードは前回説明したものと同じになります。一方、7行目のマクロが定義されている場合、前回のコードは63行目にあるコードへ置き換えられます。ここに注目してください。65行目から73行目までのコードを、63行目のコードへ置き換えているのです。それ以外の部分はすべて変更されず、そのまま残ります。
では、63行目の内容を見てみましょう。これはforループです。65行目のコードと非常によく似ています。もちろん違いがあります。65行目ではCostを呼び出していますが、63行目ではCost_2を呼び出しています。それ以外の部分では、コードは同じです。「しかし、それではなぜ一方が約80秒かかり、もう一方が18秒程度で済むのか説明できません。そんな違いが発生する理由が分かりません。」しかし、まさにそこが誤解している部分です。この単純な変更が、大きな違いを生みます。65行目だけを見ているように思えますが、実際にはCost関数は65行目、67行目、68行目、69行目の計4回呼び出されています。
これらの各呼び出しでは、テーブル全体、データベース全体、あるいは今回の場合は学習配列全体を解析する必要があります。少し考えてみてください。学習配列には、わずか4行のデータしかありません。これはコードの9行目で確認できます。たった4行です。しかし、19行目にあるCost関数が呼び出されるたびに、24行目のループがすべてのデータを再び読み込み、計算します。これは、OnStart手続き内に存在するすべての呼び出しに適用されます。
このようなプログラミングスタイルは、処理を大幅に簡単にします。しかし一般的には、それほど効率的ではありません。一方で、この同じスタイルによってニューロンは非常に汎用的になります。なぜなら、入力数や出力数が変化しても、その部分のコードを変更することなく対応できるからです。ここで重要になるのが、「自分が何をしているのか」、あるいは「期待される結果や目的が何なのか」を理解することです。多くの人は、19行目のこの関数を取り出し、OpenCLコードへ変換したり、並列処理システムで動作するコードへ変換したりするでしょう。場合によっては、並列処理向けのマシンは非常に高価になることもあります。
また、別の場合では、高性能なグラフィックカードだけで十分かもしれません。しかし、それでも本質的には、単に1つのものを別のものへ置き換えているだけです。なぜなら、コード自体は依然として非効率だからです。理由は単純です。コストを計算するたびに、学習に使用しているデータベース全体を走査する必要があるからです。これが数千MBのデータベースだったらどうでしょうか。
ここで63行目へ戻りましょう。一見すると、63行目には特別なものはないように見えます。しかし、ここでは特化された関数が呼び出されています。この違いにより、汎用的な実行と比較して大幅な高速化が実現します。ただし、特化型ニューロンを使用すると、プログラミングは多少複雑になります。なぜなら、ニューロンの入力数や出力数を変更する必要がある場合、63行目で呼び出されるCost_2関数のコードを完全に変更する必要があるからです。その理由は、まさにこの関数の動作のさせ方そのものにあります。では、この仕組みがどのように動作するのか確認するために、30行目へ進み、そこで実装されているCost_2を見てみましょう。ただし、説明をより明確にするため、この関数については新しいテーマとして詳しく扱うことにします。
単一ニューロンの特殊化
さて、すべてをより落ち着いて学び、実際の仕組みを理解したい方のために、Cost_2関数を2つの部分に分けて見ていきましょう。こうすることで、なぜこの関数が標準版より高速に動作するのかを、より簡単に説明できます。
さて、最初の部分は35行目から41行目です。この部分のコードは何をしているのでしょうか。ここでおこなっている処理は、以前65行目、67行目、68行目、69行目で実行していた処理と同じです。もちろん、それに加えて24行目にあるforループと同じ処理も実行しています。つまり、最終的には25行目の処理を実行していることになります。しかし、以前の方法とは異なり、今回はすべてを同時に処理しています。その結果、以前より多くの変数が必要になります。32行目で宣言されている各変数は、コードの実行速度を向上させるために使用されています。37行目では、ニューロン全体の総誤差を計算しています。37行目の計算自体は、それだけでは高速化に寄与しません。しかし、38行目、39行目、40行目では、以前65行目のforループ内で計算していたその他すべての誤差を求めています。
ここで、この問題の難しい部分に入ります。40行目は、すべてのニューロンに共通する部分です。つまり、37行目と同じように汎用的なコードです。しかし、38行目と39行目は違います。これらこそが、ニューロンを特化させる部分なのです。つまり、ニューロンにさらに多くの入力を追加する必要がある場合、その分だけ同じようなコード行を追加しなければなりません。その結果、ある目的のために作成されたニューロンは、別の用途ではほとんど役に立たなくなる可能性があります。しかし、この方法によって、特定のタスクに対して十分に最適化されていない汎用実装よりも、はるかに高速なコードを実現できます。
コードの2つ目の部分は43行目、44行目、45行目にあります。ここでは、次回のコスト関数呼び出しに使用する値を補正しています。以前は、この処理を70行目、71行目、72行目で行っていました。
このような小さなコード変更だけで、ニューロンの動作速度は大幅に向上しました。その結果、ニューロンは学習、あるいはより正確に言えば、事前知識として保存される値の集合を生成する処理を、より効率的に実行できるようになります。そして、ここにこの種のプログラミングの本当の魅力があります。重要なのは単なるテストとして関数を呼び出すことではありません。すべてを、より効率的に実現できることに気づく楽しさなのです。注目してほしいのは、OpenCLを使用する必要も、並列化コードを書く必要もなかったという点です。これは、多くの人が、特にニューラルネットワークの話題になると見せるあの退屈さよりも、はるかに興味深く、楽しいものです。
さて、これはどのように実現できるかを示す非常に単純な例でした。しかし、同じ問題について、さらに少し深く考えてみましょう。この単純なニューロンでも、かなり多くのことができます。唯一の制限は、入力数を増減させる必要がある場合、そのたびに可能な限り高速に対応しなければならないという点です。しかし、それでも、このニューロンは特定の種類の状況を処理するには、まだ十分に「知的」ではありません。だからこそ、人工知能やニューラルネットワークは、多くの人が考えるほど単純なものではありません。同様に、ある特定の状況において、1つのニューロンがあらゆる問題に対応できると考えることもできません。
これを説明するために、少し思考実験をしてみましょう。難しいことではありません。ここで示したいのは、ニューラルネットワークを学ぶ際に、多くの人が見落としている点です。多くの人はこの問題について沈黙したり、話したがらなかったりします。しかし、これを理解すると、どのようなプログラムも、それを作り出した人間の思考を超えることはできないということが分かります。
では、学習に使用するデータについて考えてみましょう。ニューロンがデータを処理できない状況があります。そして、それはデータが非常に複雑だからではありません。実際には、データ自体は単純です。しかし、単一のニューロンでは問題を解決できません。繰り返しますが、単一ニューロンでは解決できないのです。そのため、この問題には複数のニューロンを使用する必要があります。ここでは、後ほど詳しく扱うテーマについて簡単に触れておきます。ただし、特定のケースでは、小さなニューラルネットワークで解決すべき問題を、単一ニューロンでモデル化することも可能です。すべては、解決しようとしている問題の種類によって決まります。
つまり、多くの場合、実際にはその大部分において、将来的にニューラルネットワークを実装するために使用できるニューロンを構築、あるいは正確にはプログラムすることができます。ここで言っているニューラルネットワークとは、チャットボットのようなものではありません。もっと単純なものを指しています。しかし、場合によっては、通常なら小規模なニューラルネットワークが必要になる問題を、単一ニューロンに解かせることもできます。そして、そのために大量のデータを含むBIG DATAを作成する必要もありません。この記事で示している同じコードを使って、それを実現できます。
「なるほど、今度は本当に複雑になってきました。つまり、こういうことですか。現在扱っているこのニューロンは、同じデータベースで表現できる問題を解決できない。しかし、正しい方法で扱えば、このニューロンを小さなニューラルネットワークのように動作させ、本来なら解けない問題を解決できる。そういうことですか。」はい、その通りです。注目してください。今回使用している学習データセットは、論理ゲートの真理値表に似ています。つまり、0と1の組み合わせを入力し、ニューロンに論理ゲートを表現できる方程式、より正確には線形関数を見つけさせることができます。
しかし、ここには2つのケースがあります。ただし、これらは1つとして扱うこともできます。なぜなら、互いに逆の関係にあるからです。これらの場合、この単純なニューロンは正しい方程式を見つけることができません。しかし、特定の方法で動作させるようにすれば、正しい方程式を見つけることが可能になります。
説明を分かりやすくするために、次の図を見てください。

この図には、今回扱う主な論理ゲートが示されています。緑色の部分にはゲートの入力が示されています。出力は赤色とオレンジ色の領域で示され、各図の上部にはゲートの種類が表示されています。
では、これが何を意味するのか見てみましょう。左から右へ、ゲートはAND、NAND、OR、NOR、XOR、NXOR (XNOR)の順番で並んでいます。
では、ニューラルネットワークというテーマにおいて、この図の表は私たちにとってどのような意味を持つのでしょうか。これを理解するために、もう一度ニューロンのコードを見てみましょう。9行目に注目してください。ここでは学習ルールを定義しています。そして、そこには図に示された表とまったく同じようなものがあります。
さて、ここからが面白いところです。最初の2つの値は入力データです。一方、3つ目の値は結果を表しています。コードと図内の表を見比べると、何か気づくことはありませんか。よく観察すると、学習時にANDゲートを定義していることが分かります。
学習で使用する3番目の引数の値を変更して試してみてください。すると、ニューロンに学習させることで、ほとんどすべての論理ゲートを表現できるようになることが分かります。「ほとんど」と言ったのは、ニューロンが学習できないゲートが2つ存在するからです。それが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は、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初級から中級まで:マウスイベントの処理
取引におけるニューラルネットワーク:特定データへの依存なしに時系列を汎化する(Mamba4Cast)
初級から中級まで:オブジェクト(III)
取引におけるニューラルネットワーク:適応型モード分解を用いた時系列予測(最終回)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索