お帰りなさい!パート1では、ニューラルネットワークが基本的なフィードフォワード構造を使って2つの数字を加算する方法を見ました。記事の最後では、フォワードパス、訓練ループ、損失計算、重みの更新を含む完全なC++実装も示しました。

次の質問に自然に繋がりますね:ニューラルネットワークはどのようにして訓練を通じて自ら重みを調整するのか? それをここパート2で探っていきます。

さらに進む前に、パート1の完全なC++コードを紹介します。これは、2つの数字を加算する方法を学ぶためのシンプルなニューラルネットワークを実装しています。フォワードパス、損失計算、バックプロパゲーション、重みの更新が含まれています:

#include <iostream>
#include <cmath>
using namespace std;

// シグモイド活性化関数
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// シグモイドの導関数(バックプロパゲーションで使用)
float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

int main() {
    // 3つの隠れノードのための重みとバイアスを初期化
    // これらの値は、学習プロセスをより明確に示すために手動で選ばれています。
    // 正の重みと負の重みを両方使用することで、対称性を防ぎ、多様な出発点を提供します。
    float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;      // ノード1は両方の入力から正の影響を受けます
    float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f;    // ノード2は負の影響を受け、バランスを促進します
    float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f;   // ノード3は混合されており、相互作用を捉えるのに役立ちます

    // 出力層の重みとバイアスを初期化
    float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;

    float learning_rate = 0.01f;
    int epochs = 1000; // 訓練データをループする回数

    // 訓練ループ
    for (int epoch = 0; epoch < epochs; ++epoch) {
        float total_loss = 0.0f;
        for (int a = 0; a < 100; ++a) {
            for (int b = 0; b < 100; ++b) {
                float x1 = a / 100.0f;
                float x2 = b / 100.0f;
                float target = (a + b) / 200.0f; // 目標合計を[0, 1]に正規化

                // ===== フォワードパス =====
                float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
                float h1 = sigmoid(z1);

                float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
                float h2 = sigmoid(z2);

                float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
                float h3 = sigmoid(z3);

                float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;

                float error = target - y_pred;
                total_loss += error * error; // 二乗誤差(損失)

                // ===== バックプロパゲーション - 出力層 =====
                float delta_out = error; // y_predに対する損失の導関数
                float grad_out_w1 = delta_out * h1; // 出力重み1の勾配
                float grad_out_w2 = delta_out * h2; // 出力重み2の勾配
                float grad_out_w3 = delta_out * h3; // 出力重み3の勾配
                float grad_out_b = delta_out;       // 出力バイアスの勾配

                // ===== バックプロパゲーション - 隠れ層 =====
                // 各隠れノードが誤差にどのように寄与したかを計算
                float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
                float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
                float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);

                float grad_w1_node1 = delta_h1 * x1; // ノード1のw1の勾配
                float grad_w2_node1 = delta_h1 * x2; // ノード1のw2の勾配
                float grad_b_node1 = delta_h1;       // ノード1のバイアスの勾配

                float grad_w1_node2 = delta_h2 * x1;
                float grad_w2_node2 = delta_h2 * x2;
                float grad_b_node2 = delta_h2;

                float grad_w1_node3 = delta_h3 * x1;
                float grad_w2_node3 = delta_h3 * x2;
                float grad_b_node3 = delta_h3;

                // ===== 出力層の重みを更新 =====
                weight_out_node1 += learning_rate * grad_out_w1;
                weight_out_node2 += learning_rate * grad_out_w2;
                weight_out_node3 += learning_rate * grad_out_w3;
                bias_out += learning_rate * grad_out_b;

                // ===== 隠れ層の重みを更新 =====
                weight1_node1 += learning_rate * grad_w1_node1;
                weight2_node1 += learning_rate * grad_w2_node1;
                bias_node1 += learning_rate * grad_b_node1;

                weight1_node2 += learning_rate * grad_w1_node2;
                weight2_node2 += learning_rate * grad_w2_node2;
                bias_node2 += learning_rate * grad_b_node2;

                weight1_node3 += learning_rate * grad_w1_node3;
                weight2_node3 += learning_rate * grad_w2_node3;
                bias_node3 += learning_rate * grad_b_node3;
            }
        }
        // 100エポックごとに損失をログ出力
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[サマリー] エポック " << epoch + 1 << ": 損失 = " << total_loss << endl;
    }

    // ===== ユーザー入力でモデルをテスト =====
    float a, b;
    cout << "
合計予測のテスト (a + b)
aを入力してください: ";
    cin >> a;
    cout << "bを入力してください: ";
    cin >> b;

    float x1 = a / 100.0f;
    float x2 = b / 100.0f;

    // 訓練された重みで再度フォワードパス
    float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
    float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
    float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
    float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
    float predicted_sum = y_pred * 200.0f;

    // 結果を出力
    cout << "予測された合計: " << predicted_sum << "
実際の合計: " << (a + b) << endl;
    return 0;
}

このパートでは、ニューラルネットワークがどのように学ぶかを分解します:層と活性化関数の設定から、バックプロパゲーションを通じて重みを調整するまで。学習率とエポックを使用してモデルを訓練する方法も学び、ネットワークが加算のような単純なタスクを超えて学ぶことができるかどうかを探ります。

  • ニューラルネットワークにおけるノードと層の実際の役割。
  • 初期重みの設定と訓練中の調整方法。
  • 学習率とエポックが学習プロセスに与える役割。
  • 活性化関数とは何か、それがどのように機能し、シグモイド、tanh、またはReLUをいつ使用するか。
  • バックプロパゲーションと導関数がネットワークに誤りから学ばせる方法。
  • ニューラルネットワークが加算のような単純なタスクを超えて、複数のことを学ぶことができるかどうか。

準備はいいですか?さあ、行きましょう!

ノードと層:実際に何が起こっているのか?

ニューロン(またはノード)は、ニューラルネットワークの基本的な構成要素です。各ニューロンは入力値を受け取り、それを重みで掛け算し、バイアスを加え、その結果を活性化関数を通して出力を生成します。これは、データを段階的に変換する小さな意思決定者のようなものです。

層は、ネットワークの同じレベルで動作するニューロンの集合です。情報は1つの層から次の層へ流れます。主な層のタイプは3つあります:

  • 入力層: 生データを受け取ります(例:加算したい数字)。
  • 隠れ層: 入力に対して変換を行い、重み付き接続を通じてパターンを学習します。
  • 出力層: 最終的な予測または結果を生成します。

各層のニューロンは次の層のニューロンに接続されており、それぞれの接続にはネットワークが学習し、訓練中に調整する重みがあります。

ニューラルネットワークの各ノード(ニューロン)を小さな意思決定者と考えてください。入力を受け取り、それを「重み」で掛け算し、「バイアス」を加え、次に活性化関数(シグモイド、tanh、またはReLUなど)を適用します。

ノードが多いほど、脳力も増す:

  • 少数のノードで単純な問題を迅速に解決できるかもしれません。
  • ノードが多いほど、ネットワークはより複雑なパターンを理解できますが、あまりにも多いと遅くなったり、過剰に考えすぎたり(オーバーフィッティング)する可能性があります!

層を重ねたらどうなるでしょう? 複数の隠れ層を重ねることができます。それが「ディープニューラルネットワーク」です。各層は、少しずつ抽象的な何かを学びます:

  • 最初の層は単純なパターンを学びます。
  • 2層目はそれらのパターンを組み合わせて、より複雑なアイデアを形成します。
  • 3層目以降は、ますます抽象的な理解を構築します。

ニューラルネットワークにおける重みとバイアス

ニューラルネットワークにおける重みとは何ですか?

ニューラルネットワークにおける重みは、2つのニューロン間の接続の強さを表す値です。入力データがネットワークを流れるとき、それはこれらの重みで掛け算されます。重みが大きいほど、入力は次のニューロンの出力に強い影響を与えます。訓練中に、これらの重みはネットワークがより良い予測を行うのを助けるために調整されます。

ニューラルネットワークにおけるバイアスとは何ですか?

バイアスは、ニューラルネットワークにおける追加のパラメータで、ニューロンの活性化をシフトさせることができます。これは、オフセットや閾値のように機能し、モデルがデータにより適合するのを助け、原点を通過しないパターンを学習できるようにします。重みと同様に、バイアスも訓練中に調整され、予測を改善します。

初期重みはどのように設定しますか?

訓練を開始する際、重みは何らかの値に初期化する必要があります。私たちのコード例では、最初に手動で設定しました:

float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;

より複雑なシステムでは、重みは通常、-0.5から0.5の間の小さな値を使用してランダムに初期化されます。このランダム性は、すべてのニューロンが同じことを学ぶ対称性を防ぎ、ネットワークが有用なパターンを発見するチャンスを高めます。一般的な初期化戦略には、安定した信号をネットワーク内で流すことを目的としたXavier(Glorot)とHe初期化があります。私たちのようなシンプルな実験には、手動または小さなランダム値が十分に機能します。

重みはどのように調整しますか?

訓練中、調整の流れは次のようになります:

  1. ネットワークは現在の重みを使用して予測を行います。
  2. 予測を正しい答え(ターゲット出力)と比較します。
  3. どれだけ間違っていたかを計算します。これが誤差または「損失」です。
  4. バックプロパゲーションを使用して、各重みが誤差にどのように影響したかを計算します。
  5. 次回のために、各重みをわずかに調整します。このステップは、勾配(導関数)と学習率を使用して行います。

私たちのC++コードの例では、このプロセスはネストされた訓練ループの中で行われます。こちらが簡単な参照です:

float error = target - y_pred;
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;

float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);

float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;

// 重みを更新
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;

weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;

このスニペットは、出力層と隠れ層の重みを更新するために勾配を計算し、それを適用する方法を示しています。 各反復は、活性化関数の導関数と誤差から計算された勾配を使用して、損失を減少させる方向に重みを押し出します。数千回の反復を経て、重みは徐々に正確になります。

活性化関数:ノードに個性を与える

活性化関数は、ニューロンが「発火」すべきかどうかを決定します。これは、各ノードの重み付き合計とバイアスの結果に適用され、ネットワークに非線形性を導入します。これがなければ、ネットワークは線形関係しか学習できず(複雑な問題にはあまり役立ちません)。

ここでは、一般的な活性化関数をいくつか紹介します:

1. シグモイド

  • :   1 / (1 + e^(-x))
  • 出力範囲: 0から1
  • 使用例: バイナリ分類や、0と1の間の値が必要な出力層に最適です。
  • 利点: 滑らかで制約された出力。
  • 欠点: 極端な値で飽和し(0または1に近い)、消失勾配のために学習が遅くなります。

2. tanh

  • (e^x - e^(-x)) / (e^x + e^(-x))
  • 出力範囲: -1から1
  • 使用例: ゼロ中心の出力が必要な場合。
  • 利点: シグモイドよりも強い勾配;隠れ層での学習が速い。
  • 欠点: 極端な値で消失勾配が発生します。

3. ReLU(整流線形ユニット)

  • :   max(0, x)
  • 出力範囲: 0から無限大。
  • 使用例: ディープラーニングのほとんどの隠れ層のデフォルト。
  • 利点: 計算が速く、スパースな活性化を助けます。
  • 欠点: 入力が常に負の場合、「死んだReLU問題」が発生する可能性があります。

どの活性化関数をいつ使用するか?

活性化出力範囲適しているもの一般的な層のタイプメモ
シグモイド0から1バイナリ出力出力層消失勾配を引き起こす可能性があります
tanh-1から1ゼロ中心の隠れ層隠れ層シグモイドよりも強い勾配
ReLU0から ∞ほとんどの深層ネットワークとCNN隠れ層速く、効率的ですが、負の値で「死ぬ」可能性があります

私たちの例のコードでは、隠れ層にシグモイドを使用しました。これは小さなデモには機能しますが、より大きなネットワークでは、ReLUがより良いパフォーマンスと速い訓練のために好まれます。

導関数とバックプロパゲーション:ネットワークが誤りから学ぶ方法

目を閉じて丘の上に立ち、下り道を見つける必要があると想像してください。足で周りを感じて、どの方向が下に傾いているかを確認し、それに応じて動きます。

それがニューラルネットワークが導関数を使用して行うことです。小さな調整が予測をより正確にするのにどれだけ役立つかを確認します。

予測を行った後、ネットワークはどれだけ間違っていたかを計算します(これを「損失」と呼びます)。その後、バックプロパゲーションと呼ばれるものを使用して、その誤差をネットワークを通じて逆にたどり、各重みが誤りにどのように寄与したかを正確に把握します。

ここに簡略化されたバックプロパゲーションループがあります:

  1. フォワードパス:答えを推測します。
  2. 損失を計算します:その推測はどれだけ間違っていましたか?
  3. バックワードパス:各重みがその誤差にどのように影響したかを調べます。
  4. 重みを更新します:将来の誤りを減らすために重みをわずかに調整します。

このプロセスは何千回も繰り返され、ネットワークは徐々に賢くなります。

このアイデアの簡略化されたC++実装を見てみましょう:

float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

これが私たちの活性化関数とその導関数です。これらはフォワードパスとバックワードパスの両方で使用されます。

訓練ループは2つの入力(aとb)を使用し、それらを0から1の間に正規化し、ネットワークにその合計を予測させる訓練を行います。隠れノードは3つ、出力ノードは1つです。ループの重要な部分は次のとおりです:

float error = target - y_pred;
total_loss += error * error;

float delta_out = error;
...
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);

ここで、誤差を計算し、導関数を使用して調整をネットワークに押し戻します—バックプロパゲーションの実行です。

学習率とエポック

コードで気づいたかもしれませんが、訓練プロセスを制御するために2つの重要なハイパーパラメータを使用しています:learning_rate とエポックです。

学習率とは何ですか?

学習率は、重みを更新する際の各ステップの大きさを決定します。小さな学習率(0.01のような)は、ネットワークが毎回わずかな調整を行うことを意味し、安全ですが遅くなります。大きな学習率は物事を速くするかもしれませんが、あまりにも大きいと、ネットワークがオーバーシュートして適切に学習できなくなる可能性があります。

私たちのコードでは、次のように使用しています:

float learning_rate = 0.01f;

これにより、速度と安定性のバランスが取れます。

エポックとは何ですか?

エポックは、訓練データセット全体を1回通過することを指します。私たちの例では、各エポックで、0から99までのすべてのの組み合わせを使用してネットワークを訓練します。そして、次のエポックのために再度行います。

ネットワークが重みを何度も洗練するために、複数のエポックで訓練します。

  • エポック数を増やすと、ネットワークは改善の機会が増え、損失をさらに減少させる可能性があります。ただし、あまりにも多くのエポックはオーバーフィッティングを引き起こす可能性があり、モデルが訓練データに過剰に適合し、新しい入力に対してパフォーマンスが低下します。
  • エポック数を減らすと、訓練が速くなりますが、ネットワークが十分に学習できず、パフォーマンスが低下する可能性があります。

コードでは、次のように使用しています:

int epochs = 1000;

これは、加算のような単純な問題に対して通常良いスタートですが、損失がどれだけ早くまたは遅く減少するかに応じて調整できます。

ニューラルネットワークは複数のことを学べるか?

もちろんです。ニューラルネットワークは、1つのタイプのタスクだけを学ぶことに制限されていません。実際、同じネットワークアーキテクチャは、与えられたデータと訓練方法に応じて、手書きの数字を認識したり、言語を翻訳したり、音楽を生成したりするなど、さまざまなことを行うように訓練できます。

私たちの例では、ネットワークは2つの数字を加算することを学びましたが、異なる訓練データと修正された出力層を使用すれば、次のことも学ぶことができます:

  • 数字を引き算する
  • 画像を分類する
  • 時系列の将来の値を予測する

ネットワークが処理するタスクが多いほど、またはタスクが複雑になるほど、必要なニューロンや層が増える可能性があります。加算のような単純な問題には少数のノードで十分かもしれませんが、画像認識や言語翻訳のようなタスクには、はるかに深く広いネットワークが必要になる場合があります。ただし、複雑さが常に良いとは限りません。ノードを増やしすぎると、ネットワークが記憶するだけでなく、一般化することを学ぶことができなくなるオーバーフィッティングが発生する可能性があります。

これは、加算と減算の両方を入力数字に対して訓練する単一の共有ネットワークの強化版です:


#include <iostream>
#include <cmath>
using namespace std;

// シグモイド活性化関数(入力を[0, 1]の範囲に圧縮)
float sigmoid(float x) {
    return 1.0f / (1.0f + exp(-x));
}

// シグモイドの導関数(バックプロパゲーション中の勾配計算に使用)
float sigmoid_derivative(float y) {
    return y * (1 - y); // y = sigmoid(x)
}

int main() {
    // === 隠れ層の重みとバイアス(3つの隠れニューロン、各2つの入力) ===
    float w1_n1 = 0.5f, w2_n1 = 0.5f, b1 = 0.0f;
    float w1_n2 = -0.5f, w2_n2 = -0.5f, b2 = 0.0f;
    float w1_n3 = 0.25f, w2_n3 = -0.25f, b3 = 0.0f;

    // === 出力層の重みとバイアス(2つの出力ニューロン:合計と引き算) ===
    float out_w1_sum = 0.5f, out_w2_sum = 0.5f, out_w3_sum = 0.5f, out_b_sum = 0.0f;
    float out_w1_sub = -0.5f, out_w2_sub = -0.5f, out_w3_sub = 0.5f, out_b_sub = 0.0f;

    float learning_rate = 0.01f;
    int epochs = 1000;

    // === 訓練ループ ===
    for (int epoch = 0; epoch < epochs; ++epoch) {
        float total_loss = 0.0f;

        for (int a = 0; a < 100; ++a) {
            for (int b = 0; b < 100; ++b) {
                // === 入力値を正規化 ===
                float x1 = a / 100.0f;
                float x2 = b / 100.0f;

                // === 目標を正規化 ===
                float target_sum = (a + b) / 200.0f;          // 合計は0から198の範囲 → [0, ~1]
                float target_sub = (a - b + 100.0f) / 200.0f; // 引き算は-99から+99の範囲 → [0, ~1]

                // === フォワードパス(入力 → 隠れ層) ===
                float z1 = x1 * w1_n1 + x2 * w2_n1 + b1;
                float h1 = sigmoid(z1);

                float z2 = x1 * w1_n2 + x2 * w2_n2 + b2;
                float h2 = sigmoid(z2);

                float z3 = x1 * w1_n3 + x2 * w2_n3 + b3;
                float h3 = sigmoid(z3);

                // === フォワードパス(隠れ層 → 出力層) ===
                float y_pred_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
                float y_pred_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

                // === 損失を計算(平方誤差) ===
                float error_sum = target_sum - y_pred_sum;
                float error_sub = target_sub - y_pred_sub;
                total_loss += error_sum * error_sum + error_sub * error_sub;

                // === バックプロパゲーション - 出力層(各出力ノードの勾配) ===
                float delta_out_sum = error_sum;
                float grad_out_w1_sum = delta_out_sum * h1;
                float grad_out_w2_sum = delta_out_sum * h2;
                float grad_out_w3_sum = delta_out_sum * h3;
                float grad_out_b_sum = delta_out_sum;

                float delta_out_sub = error_sub;
                float grad_out_w1_sub = delta_out_sub * h1;
                float grad_out_w2_sub = delta_out_sub * h2;
                float grad_out_w3_sub = delta_out_sub * h3;
                float grad_out_b_sub = delta_out_sub;

                // === バックプロパゲーション - 隠れ層 ===
                float delta_h1 = (delta_out_sum * out_w1_sum + delta_out_sub * out_w1_sub) * sigmoid_derivative(h1);
                float delta_h2 = (delta_out_sum * out_w2_sum + delta_out_sub * out_w2_sub) * sigmoid_derivative(h2);
                float delta_h3 = (delta_out_sum * out_w3_sum + delta_out_sub * out_w3_sub) * sigmoid_derivative(h3);

                float grad_w1_n1 = delta_h1 * x1;
                float grad_w2_n1 = delta_h1 * x2;
                float grad_b1 = delta_h1;

                float grad_w1_n2 = delta_h2 * x1;
                float grad_w2_n2 = delta_h2 * x2;
                float grad_b2 = delta_h2;

                float grad_w1_n3 = delta_h3 * x1;
                float grad_w2_n3 = delta_h3 * x2;
                float grad_b3 = delta_h3;

                // === 出力重みを更新 ===
                out_w1_sum += learning_rate * grad_out_w1_sum;
                out_w2_sum += learning_rate * grad_out_w2_sum;
                out_w3_sum += learning_rate * grad_out_w3_sum;
                out_b_sum += learning_rate * grad_out_b_sum;

                out_w1_sub += learning_rate * grad_out_w1_sub;
                out_w2_sub += learning_rate * grad_out_w2_sub;
                out_w3_sub += learning_rate * grad_out_w3_sub;
                out_b_sub += learning_rate * grad_out_b_sub;

                // === 隠れ重みを更新 ===
                w1_n1 += learning_rate * grad_w1_n1;
                w2_n1 += learning_rate * grad_w2_n1;
                b1 += learning_rate * grad_b1;

                w1_n2 += learning_rate * grad_w1_n2;
                w2_n2 += learning_rate * grad_w2_n2;
                b2 += learning_rate * grad_b2;

                w1_n3 += learning_rate * grad_w1_n3;
                w2_n3 += learning_rate * grad_w2_n3;
                b3 += learning_rate * grad_b3;
            }
        }

        // === 100エポックごとに損失を表示 ===
        if ((epoch + 1) % 100 == 0 || epoch == 0)
            cout << "[エポック " << epoch + 1 << "] 損失: " << total_loss << endl;
    }

    // === テストフェーズ ===
    float a, b;
    cout << "\n予測テスト\n aを入力してください: ";
    cin >> a;
    cout << "bを入力してください: ";
    cin >> b;

    float x1 = a / 100.0f;
    float x2 = b / 100.0f;

    // ユーザー入力のために再度フォワードパス
    float h1 = sigmoid(x1 * w1_n1 + x2 * w2_n1 + b1);
    float h2 = sigmoid(x1 * w1_n2 + x2 * w2_n2 + b2);
    float h3 = sigmoid(x1 * w1_n3 + x2 * w2_n3 + b3);

    float y_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
    float y_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;

    // 出力を非正規化
    float predicted_sum = y_sum * 200.0f;
    float predicted_sub = y_sub * 200.0f - 100.0f;

    cout << "予測された合計:        " << predicted_sum << endl;
    cout << "実際の合計:           " << (a + b) << endl;
    cout << "予測された差: " << predicted_sub << endl;
    cout << "実際の差:    " << (a - b) << endl;

    return 0;
}

まとめ

たくさんのことを学びましたが、ちょっと素晴らしいですよね?理論だけでなく、ニューラルネットワークが実際に何かを学ぶ様子を見ました:数字を加算したり引き算したりすること。ルールはハードコーディングされていません—訓練を通じて重みが自ら調整されます。その過程で、学習率、活性化関数、そしてすべてを結びつけるバックプロパゲーションの魔法を知ることができました。

次のパートでは、コードから一歩引いて、このすべての背後にあるストーリーに飛び込みます。ニューラルネットワークはどこから来たのか?なぜAIはここ数年で爆発的に成長したのか?何が変わったのか?すべての始まりと、それがまだ始まったばかりである理由を探ります。