<jupyter_start><jupyter_text>Copyright 2021-2022 Digital Advantage - Deep Insider.<jupyter_code>#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.<jupyter_output><empty_output><jupyter_text>連載『ニューラルネットワーク入門』 「コードで必ず分かるニューラルネットワーク（DNN）の逆伝播」        Deep Insiderで記事を読む         Google Colabで実行する         AWS  SageMaker Studio Labで実行する         GitHubでソースコードを見る   ※上から順に実行してください。上のコードで実行したものを再利用しているところがあるため、すべて実行しないとエラーになるコードがあります。  　すべてのコードを一括実行したい場合は、Colabであればメニューバーから［ランタイム］－［すべてのセルを実行］をクリックしてください。  ■本ノートブックの目的 ニューラルネットワーク（以下、ニューラルネット）の仕組みを、数学理論からではなく**Pythonコードから学ぶ**ことを狙っています。「難しい高校以降の数学は苦手だけど、コードなら読めるぜ！」という方にピッタリです。  ●本ノートブックの特徴 - 線形代数（linear algebra、行列演算）を使いません。つまり、NumPyを使いません。- 基本的に掛け算や足し算などの中学までの数学のみで、ニューラルネットのロジックをコーディングしていきます。- ※微分の導関数は、そのままコードとして記載することで、微分の計算は取り上げません。![図1　線形代数を使わないことによるメリット](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/01-forward-prop/images/01.png)  ■ニューラルネットワークの図 基本的なニューラルネット（この例では、入力層：2、隠れ層：3、出力層：1）の図を確認しておきます。![図2　ニューラルネットワークの図（左：横描き、右：縦描き）](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/01-forward-prop/images/02.png)  ■訓練（学習）処理全体の実装 深層「学習」のメインは、ニューラルネットの「訓練」処理ですよね。ここから書いていきます。<jupyter_code># 取りあえず仮で、空の関数を定義して、コードが実行できるようにしておく
def forward_prop(cache_mode=False):
    " 順伝播を行う関数。"
    return None, None, None

y_true = [1.0]  # 正解値
def back_prop(y_true, cached_outs, cached_sums):
    " 逆伝播を行う関数。"
    return None, None

LEARNING_RATE = 0.1 # 学習率（lr）
def update_params(grads_w, grads_b, lr=0.1):
    " パラメーター（重みとバイアス）を更新する関数。"
    return None, None

# ---ここまでは仮の実装。ここからが必要な実装---

# 訓練処理
y_pred, cached_outs, cached_sums = forward_prop(cache_mode=True)  # （1）
grads_w, grads_b = back_prop(y_true, cached_outs, cached_sums)  # （2）
weights, biases = update_params(grads_w, grads_b, LEARNING_RATE)  # （3）

print(f'予測値：{y_pred}')  # 予測値： None
print(f'正解値：{y_true}')  # 正解値： [1.0]<jupyter_output><empty_output><jupyter_text>ニューラルネットの訓練に必要なことは、以下の3つだけ。1. **順伝播：** `forward_prop()◎`数として実装2. **逆伝播：** `back_prop()`関数として実装。損失（予測と正解の誤差）の計算はここで行う3. **パラメーター（重みとバイアス）の更新：** `update_params()`関数として実装。これによりモデルが**最適化**される ![図3　訓練（学習）処理を示したニューラルネットワーク図](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/01-forward-prop/images/03.png) ■モデルの定義と、仮の訓練データ 入力層のノードが2個、隠れ層のノードが3個、出力層のノードが1個のモデル（`model`変数）を定義しましょう。<jupyter_code># ニューラルネットワークは3層構成
layers = [
    2,  # 入力層の入力（特徴量）の数
    3,  # 隠れ層1のノード（ニューロン）の数
    1]  # 出力層のノードの数

# 重みとバイアスの初期値
weights = [
    [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], # 入力層→隠れ層1
    [[0.0, 0.0, 0.0]] # 隠れ層1→出力層
]
biases = [
    [0.0, 0.0, 0.0],  # 隠れ層1
    [0.0]  # 出力層
]

# モデルを定義
model = (layers, weights, biases)

# 仮の訓練データ（1件分）を準備
x = [0.05, 0.1]  # x_1とx_2の2つの特徴量<jupyter_output><empty_output><jupyter_text>■ステップ1. 順伝播の実装  ●1つのノードにおける順伝播の処理 ニューラルネットの最小単位である「1つのノード」における順伝播の処理をコーディングしましょう。<jupyter_code># 取りあえず仮で、空の関数を定義して、コードが実行できるようにしておく
def summation(x,weights, bias):
    " 重み付き線形和の関数。"
    return 0.0

def sigmoid(x):
    " シグモイド関数。"
    return 0.0

def identity(x):
    " 恒等関数。"
    return 0.0


w = [0.0, 0.0]  # 重み（仮の値）
b = 0.0  # バイアス（仮の値）

next_x = x  # 訓練データをノードへの入力に使う

# ---ここまでは仮の実装。ここからが必要な実装---

# 1つのノードの処理（1）： 重み付き線形和
node_sum = summation(next_x, w, b)

# 1つのノードの処理（2）： 活性化関数
is_hidden_layer = True
if is_hidden_layer:
    # 隠れ層（シグモイド関数）
    node_out = sigmoid(node_sum)
else:
    # 出力層（恒等関数）
    node_out = identity(node_sum)<jupyter_output><empty_output><jupyter_text>1つのノードの順伝播処理に必要なことは、以下の2つの数学関数だけ。1. **重み付き線形和の関数：** `summation()`関数として実装2. **活性化関数：** ここでは`sigmoid()`関数や`identity()`関数として実装 ![図4　1つのニューロンにおける順伝播の処理を示した図](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/01-forward-prop/images/04.png)  ●重み付き線形和 重み付き線形和（weighted linear summation、以下では「線形和」と表記）とは、とは、あるノードへの複数の入力（$x_1$、$x_2$など）に、それぞれの重み（$w_1$、$w_2$など）を掛けて足し合わせて、最後にバイアス（$b$）を足した値です（上の図の左）。<jupyter_code>def summation(x, weights, bias):
    """
    重み付き線形和の関数。
    ※1データ分、つまりxとweightsは「一次元リスト」という前提。
    - 引数：
    x： 入力データをリスト値（各要素はfloat値）で指定する。
    weights： 重みをリスト値（各要素はfloat値）で指定する。
    bias： バイアスをfloat値で指定する。
    - 戻り値：
    線形和の計算結果をfloat値で返す。
    """
    linear_sum = 0.0
    for x_i, w_i in zip(x, weights):
        linear_sum += x_i * w_i  # iは「番号」（数学は基本的に1スタート）
        # print(f'x_i({x_i})×w_i({w_i})＋', end='')
    linear_sum += bias
    # print(f'b({bias})', end='')
    return linear_sum

# 線形代数を使う場合のコード例：
# linear_sum = np.dot(x, weights) + bias<jupyter_output><empty_output><jupyter_text>ついで、次回の逆伝播（の中で使う偏微分）で必要となる線形和（linear **sum**mation）の偏導関数（partial **der**ivative function）を実装しておきます。<jupyter_code>def sum_der(x, weights, bias, with_respect_to='w'):
    """
    重み付き線形和の関数の偏導関数。
    ※1データ分、つまりxとweightsは「一次元リスト」という前提。
    - 引数：
    x： 入力データをリスト値で指定する。
    weights：  重みをリスト値で指定する。
    bias: バイアスをfloat値で指定する。
    with_respect_to: 何に関して偏微分するかを指定する。
        'w'＝ 重み、'b'＝ バイアス、'x'＝ 入力。
    - 戻り値：
    with_respect_toが'w'や'x'の場合はリスト値で、'b'の場合はfloat値で
        線形和の偏微分の計算結果（偏微分係数）を返す。
    """    
    if with_respect_to == 'w':
        return x  # 線形和uを各重みw_iで偏微分するとx_iになる（iはノード番号）
    elif with_respect_to == 'b':
        return 1.0  # 線形和uをバイアスbで偏微分すると1になる
    elif with_respect_to == 'x':
        return weights  # 線形和uを各入力x_iで偏微分するとw_iになる<jupyter_output><empty_output><jupyter_text>●活性化関数：シグモイド関数 隠れ層では、最も基礎的なシグモイド関数（Sigmoid function）を固定的に使うことにします。導関数も実装しておきます。<jupyter_code>import math

def sigmoid(x):
    """
    シグモイド関数。
    - 引数：
    x： 入力データをfloat値で指定する。
    - 戻り値：
    シグモイド関数の計算結果をfloat値で返す。
    """
    return 1.0 / (1.0 + math.exp(-x))

# 線形代数の場合はmathをnpに変える（事前にimport numpy as np）
def sigmoid_der(x):
    """
    シグモイド関数の（偏）導関数。
    - 引数：
    x： 入力データをfloat値で指定する。
    - 戻り値：
    シグモイド関数の（偏）微分の計算結果（微分係数）をfloat値で返す。
    """
    output = sigmoid(x)
    return output * (1.0 - output)<jupyter_output><empty_output><jupyter_text>●活性化関数：恒等関数 出力層では、回帰問題をイメージして、そのままの値を出力する活性化関数である恒等関数（Identity function）を使用します。導関数も実装しておきます。<jupyter_code>def identity(x):
    """
    恒等関数の関数。
    - 引数：
    x： 入力データをfloat値で指定する。
    - 戻り値：
    恒等関数の計算結果（そのまま）をfloat値で返す。
    """
    return x
def identity_der(x):
    """
    恒等関数の（偏）導関数。
    - 引数：
    x： 入力データをfloat値で指定する。
    - 戻り値：
    恒等関数の（偏）微分の計算結果（微分係数）をfloat値で返す。
    """
    return 1.0<jupyter_output><empty_output><jupyter_text>●順伝播の処理全体の実装 ニューラルネットには、層があり、その中に複数のノードが存在するという構造です。  従って、- 各層を1つずつ処理する`for`ループと、    - 層の中のノードを1つずつ処理する`for`ループの2段階構造が必要で、    - その中に「1つのノードにおける順伝播の処理」を記述すればよいわけです。<jupyter_code>def forward_prop(layers, weights, biases, x, cache_mode=False):
    """
    順伝播を行う関数。
    - 引数：
    (layers, weights, biases)： モデルを指定する。
    x： 入力データを指定する。
    cache_mode： 予測時はFalse、訓練時はTrueにする。これにより戻り値が変わる。
    - 戻り値：
    cache_modeがFalse時は予測値のみを返す。True時は、予測値だけでなく、
        キャッシュに記録済みの線形和（Σ）値と、活性化関数の出力値も返す。
    """

    cached_sums = []  # 記録した全ノードの線形和（Σ）の値
    cached_outs = []  # 記録した全ノードの活性化関数の出力値

    # まずは、入力層を順伝播する
    # print(f'■第1層（入力層）-全て（{len(x)}個）の特徴量：')
    # print(f'　●入力データ： ', end='')
    cached_outs.append(x)  # 何も処理せずに出力値を記録
    # print(f'何もしない＝out({x})')
    next_x = x  # 現在の層の出力（x）＝次の層への入力（next_x）

    # 次に、隠れ層や出力層を順伝播する
    SKIP_INPUT_LAYER = 1
    for layer_i, layer in enumerate(layers):  # 各層を処理
        if layer_i == 0:
            continue  # 入力層は上で処理済み

        # 各層のノードごとに処理を行う
        sums = []
        outs = []
        for node_i in range(layer):  # 層の中の各ノードを処理
            # print(f'■第{layer_i+1}層-第{node_i+1}ノード：')

            # ノードごとの重みとバイアスを取得
            w = weights[layer_i - SKIP_INPUT_LAYER][node_i]
            b = biases[layer_i - SKIP_INPUT_LAYER][node_i]

            # 1つのノードの処理（1）： 重み付き線形和
            # print(f'　●重み付き線形和： ', end='')
            node_sum = summation(next_x, w, b)
            # print(f'＝sum({node_sum})')

            # 1つのノードの処理（2）： 活性化関数
            if layer_i < len(layers)-1:  # -1は出力層以外の意味
                # 隠れ層（シグモイド関数）
                # print(f'　●活性化関数（隠れ層はシグモイド関数）： ', end='')
                node_out = sigmoid(node_sum)
                # print(f'sigmoid({node_sum})＝out({node_out})')
            else:
                # 出力層（恒等関数）
                # print(f'　●活性化関数（出力層は恒等関数）： ', end='')
                node_out = identity(node_sum)
                # print(f'identity({node_sum})＝out({node_out})')

            # 各ノードの線形和と（活性化関数の）出力をリストにまとめていく
            sums.append(node_sum)
            outs.append(node_out)

        # 各層内の全ノードの線形和と出力を記録
        cached_sums.append(sums)
        cached_outs.append(outs)
        next_x = outs  # 現在の層の出力（outs）＝次の層への入力（next_x）

    if cache_mode:
        return (cached_outs[-1], cached_outs, cached_sums)

    return cached_outs[-1]


# 訓練時の（1）順伝播の実行例
y_pred, cached_outs, cached_sums = forward_prop(*model, x, cache_mode=True)
# ※先ほど作成したモデルと訓練データを引数で受け取るよう改変した

print(f'cached_outs={cached_outs}')
print(f'cached_sums={cached_sums}')
# 出力例：
# cached_outs=[[0.05, 0.1], [0.5, 0.5, 0.5], [0.0]]  # 入力層／隠れ層1／出力層
# cached_sums=[[0.0, 0.0, 0.0], [0.0]]  # 隠れ層1／出力層（※入力層はない）<jupyter_output><empty_output><jupyter_text>数値が**0.0**ばかりなので、別の計算パターンのコードも入れておきました。<jupyter_code># 記事にはないが、別の計算パターンでもチェックしてみよう
x3 = [0.05, 0.1]
layers3 = [2, 2, 2]
weights3 = [
    [[0.15, 0.2], [0.25, 0.3]],
    [[0.4, 0.45], [0.5,0.55]]
]
biases3 = [[0.35, 0.35], [0.6, 0.6]]
model3 = (layers3, weights3, biases3)

y_pred3, cached_outs3, cached_sums3 = forward_prop(*model3, x3, cache_mode=True)
print(f'y_pred={y_pred3}')
print(f'cached_outs={cached_outs3}')
print(f'cached_sums={cached_sums3}')
# y_pred=[1.10590596705977, 1.2249214040964653]
# cached_outs=[[0.05, 0.1], [0.5932699921071872, 0.596884378259767], [1.10590596705977, 1.2249214040964653]]
# cached_sums=[[0.3775, 0.39249999999999996], [1.10590596705977, 1.2249214040964653]]<jupyter_output><empty_output><jupyter_text>ここまでのコード中に仕込んでいる`print()`関数（※全てコメントアウトしています）のコメントを解除すると、以下のように途中の計算内容が順番にテキスト出力されます。計算内容の検証用の機能です。 ![別の計算パターンの出力例](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/01-forward-prop/images/notebook-01.png)  ●順伝播による予測の実行例 非常にシンプルで原始的な実装ですが、このように任意の層数とノード数の全結合のDNN（Deep Neural Network）のアーキテクチャーを定義して、DNNモデルによる予測が行えます。<jupyter_code># 異なるDNNアーキテクチャーを定義してみる
layers2 = [
    2,  # 入力層の入力（特徴量）の数
    3,  # 隠れ層1のノード（ニューロン）の数
    2,  # 隠れ層2のノード（ニューロン）の数
    1]  # 出力層のノードの数

# 重みとバイアスの初期値
weights2 = [
    [[-0.2, 0.4], [-0.4, -0.5], [-0.4, -0.5]], # 入力層→隠れ層1
    [[-0.2, 0.4, 0.9], [-0.4, -0.5, -0.2]], # 隠れ層1→隠れ層2
    [[-0.5, 1.0]] # 隠れ層2→出力層
]
biases2 = [
    [0.1, -0.1, 0.1],  # 隠れ層1
    [0.2, -0.2],  # 隠れ層2
    [0.3]  # 出力層
]

# モデルを定義
model2 = (layers2, weights2, biases2)

# 仮の訓練データ（1件分）を準備
x2 = [2.3, 1.5]  # x_1とx_2の2つの特徴量

# 予測時の（1）順伝播の実行例
y_pred = forward_prop(*model2, x2)
print(y_pred)  # 予測値
# 出力例：
# [0.3828840428423274]<jupyter_output><empty_output><jupyter_text>●今後のステップの準備：関数への仮引数の追加<jupyter_code>def back_prop(layers, weights, biases, y_true, cached_outs, cached_sums):
    " 逆伝播を行う関数。"
    return None, None

def update_params(layers, weights, biases, grads_w, grads_b, lr=0.1):
    " パラメーター（重みとバイアス）を更新する関数。"
    return None, None<jupyter_output><empty_output><jupyter_text>■ステップ2. 逆伝播の実装  ●逆伝播の目的と全体像 逆伝播の目的は、誤差（厳密には予測値に関する損失関数の偏微分係数）などの数値（本ノートブックでは**誤差情報**と呼ぶ）をニューラルネットに逆方向で流すこと（＝逆伝播）によって「**重みとバイアスの勾配を計算すること**」です（下の図）。 ![図5　「逆伝播の流れ」のイメージ（左：ネットワーク図、右：対応する処理／数学計算）](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/05.png) 順伝播（`forward_prop()`関数）では、計算途中に出た計算結果である「予測値（`y_pred`）」や「各ノードでの活性化関数の出力値（`cached_outs`）」と「線形和の値（`cached_sums`）」を返すだけでした。逆伝播（`back_prop()関数`）では、計算途中に出た計算結果である「各ノードへの入力の勾配（＝逆伝播していく誤差情報）」だけでなく、「各重みの勾配（`grads_w`）」「各バイアスの勾配（`grads_b`）」の計算も必要です（下の図）。 ![図6　逆伝播では各ノードへの入力／各重み／各バイアスの勾配を計算する](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/06.png) 逆伝播では、$x_1$／$w_1$／$x_2$／$w_2$／……／$x_n$／$w_n$／$b$という大量の変数に関して、損失関数の偏微分係数（＝勾配）を計算する必要があります。  ●損失関数：二乗和誤差 損失関数として、最も基礎的な二乗和誤差（SSE：Sum of Squared Error）を使うことにします。導関数も実装しておきます。<jupyter_code>def sseloss(y_pred, y_true):
    """
    二乗和誤差（Sum of Squared Error）の関数。
    - 引数：
    y_pred： モデルの最終出力値（output）＝予測値（prediction）。
    y_true： 目的となる値（target）＝正解値（truth、label）。
    - 戻り値：
    二乗和誤差の計算結果をfloat値で返す。
    """
    return 0.5 * (y_pred - y_true) ** 2
def sseloss_der(y_pred, y_true):
    """
    二乗和誤差（Sum of Squared Error）の偏導関数。予測値（y_pred）に関して二乗和誤差関数（sseloss()）を偏微分する。
    - 引数：
    y_pred： モデルの最終出力値（output）＝予測値（predicted value）。
    y_true： 目的となる値（target）＝正解値（true/actual value、label）。
    - 戻り値：
    二乗和誤差の偏微分の計算結果（偏微分係数）をfloat値で返す。
    """
    return y_pred - y_true<jupyter_output><empty_output><jupyter_text>偏導関数の式`y_pred - y_true`は、予測値と正解値の「誤差（Error、ズレ）」となっています。**誤差逆伝播法**（error backpropagation）とは、この「誤差」の数値（厳密には、予測値に関しての損失関数の偏微分係数）が誤差情報としてニューラルネットを「逆」向きに「伝播」していく過程で、本来の目的である各重みと各バイアスの勾配を求める方法です。 ![図7　各ノードでの逆伝播の処理はワンパターン](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/07.png)  ●1つのノードにおける逆伝播の処理 `損失関数( 活性化関数( 線形和関数( 入力、重み、バイアス ) ) )`という入れ子の関数は数学で**合成関数**と呼ばれます。合成関数を微分するときの公式が**連鎖律**です。連鎖律を使うと、まるでマジックのように各関数の偏微分係数の掛け算だけの式に変化します（下の図）。 ![図8　連鎖律を使うと各関数の偏微分の掛け算になる（各重みに関して損失関数を偏微分する例）](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/08.png) 上の図は各重みに関して損失関数を偏微分する例ですが、各バイアスや各入力に関して損失関数を偏微分する際も連鎖律の形はほぼ同じです（下の図）。ただし入力については、前の層のノードごとに、今の層からの全てのエッジから来る各誤差情報（偏微分係数）を合計する必要があるので注意してください。 ![図9　各重み／バイアス／入力に関して損失関数を偏微分する場合の連鎖律の形](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/09.png) 各層における各ノードの計算は、　　「逆伝播していく誤差情報」×「活性化関数の偏微分」×「線形和関数の偏微分」という掛け算に共通化できます（下の図）。 ![図10　各層の各ノードでの計算パターンは共通化できる（出力層や隠れ層で入力の勾配を計算する例）](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/10.png) 出力層から隠れ層まで全て、以下の4工程のワンパターンで実装できます（下の図）。1. 逆伝播していく誤差情報2. 活性化関数を偏微分3. 線形和を重み／バイアス／入力で偏微分4. 各重み／バイアス／各入力の勾配を計算 ![図11　「逆伝播の流れ」の実装内容](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/11.png)  ●（1）逆伝播していく誤差情報<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
layer_i = 2  # 2：出力層、1：隠れ層1、0：入力層
layer_max_i = 2  # 最後の層（＝出力層）のインデックス
is_output_layer = (layer_i == layer_max_i)  # 出力層か（True）、隠れ層か（False）

# 入力層／隠れ層1／出力層にある各ノードの（活性化関数の）出力値
cached_outs = [[0.05, 0.1], [0.5, 0.5, 0.5], [0.0]]
y_true = [1.0]  # 正解値
grads_x = []  # 入力の勾配
# ---ここまでは仮の実装。ここからが必要な実装---

if is_output_layer:
    # 出力層（損失関数の偏微分係数）
    back_error = []  # 逆伝播していく誤差情報
    y_pred = cached_outs[layer_i]
    for output, target in zip(y_pred, y_true):
        loss_der = sseloss_der(output, target)  # 誤差情報
        back_error.append(loss_der)
else:
    # 隠れ層（次の層への入力の偏微分係数）
    back_error = grads_x[-1]  # 最後に追加された入力の勾配<jupyter_output><empty_output><jupyter_text>※（1）は層ごとにまとめての処理です。以下からの（2）～（4）はノードごとの処理になります。  ●（2）活性化関数を偏微分<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
SKIP_INPUT_LAYER = 1  # 入力層を飛ばす
cached_sums = [[0.0, 0.0, 0.0], [0.0]]  # 隠れ層1／出力層（※入力層はない）
node_sum = cached_sums[layer_max_i - SKIP_INPUT_LAYER]  # 出力層
# ---ここまでは仮の実装。ここからが必要な実装---

if is_output_layer:
    # 出力層（恒等関数の微分）
    active_der = identity_der(node_sum)
else:
    # 隠れ層（シグモイド関数の微分）
    active_der = sigmoid_der(node_sum)<jupyter_output><empty_output><jupyter_text>●（3）線形和を重み／バイアス／入力で偏微分<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
PREV_LAYER = 1  # 前の層を指定するため
node_i = 0  # ノード番号

# 重みとバイアスの初期値
weights = [
    [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], # 入力層→隠れ層1
    [[0.0, 0.0, 0.0]] # 隠れ層1→出力層
]
biases = [
    [0.0, 0.0, 0.0],  # 隠れ層1
    [0.0]  # 出力層
]
# 入力層／隠れ層1／出力層にある各ノードの（活性化関数の）出力値
cached_outs = [[0.05, 0.1], [0.5, 0.5, 0.5], [0.0]]
# ---ここまでは仮の実装。ここからが必要な実装---

w = weights[layer_i - SKIP_INPUT_LAYER][node_i]
b = biases[layer_i - SKIP_INPUT_LAYER][node_i]
x = cached_outs[layer_i - PREV_LAYER]  # 前の層の出力（out）＝今の層への入力（x）
sum_der_w = sum_der(x, w, b, with_respect_to='w')
sum_der_b = sum_der(x, w, b, with_respect_to='b')
sum_der_x = sum_der(x, w, b, with_respect_to='x')<jupyter_output><empty_output><jupyter_text>![図12　「逆伝播していく誤差情報」「活性化関数を偏微分」「線形和を偏微分」まで実装完了](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/12.png)  ●（4）各重み／バイアス／各入力の勾配を計算 まずは共通の計算部分であるデルタ（`delta`変数）を計算します。<jupyter_code>delta = back_error[node_i] * active_der<jupyter_output><empty_output><jupyter_text>![図13　デルタ（delta）のイメージ](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/13.png) 次にバイアスの勾配（`grad_b`変数）を計算します。<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
layer_grads_b = []  # 層ごとの、バイアス勾配のリスト
# ---ここまでは仮の実装。ここからが必要な実装---

# バイアスは1つだけ
grad_b = delta * sum_der_b
layer_grads_b.append(grad_b)<jupyter_output><empty_output><jupyter_text>![図9（再掲）　各重み／バイアス／入力に関して損失関数を偏微分する場合の連鎖律の形](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/02-back-prop/images/09.png) 最後に各重みの勾配（`grad_w`変数）と各入力の勾配（`grad_x`変数）を計算します。<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
layer_grads_w = []  # 層ごとの、重み勾配のリスト
layer_grads_x = []  # 層ごとの、入力勾配のリスト
# ---ここまでは仮の実装。ここからが必要な実装---

# 重みと入力は前の層のノードの数だけある
node_grads_w = []
for x_i, (each_dw, each_dx) in enumerate(zip(sum_der_w, sum_der_x)):
    # 重みは個別に取得する
    grad_w = delta * each_dw
    node_grads_w.append(grad_w)

    # 入力は各ノードから前のノードに接続する全ての入力を合計する
    # （※重み視点と入力視点ではエッジの並び方が違うので注意）
    grad_x = delta * each_dx
    if node_i == 0:
        # 最初に、入力の勾配を作成
        layer_grads_x.append(grad_x)
    else:
        # その後は、その入力の勾配に合計していく
        layer_grads_x[x_i] += grad_x
layer_grads_w.append(node_grads_w)<jupyter_output><empty_output><jupyter_text>●逆伝播の処理全体の実装 ニューラルネットには、層があり、その中に複数のノードが存在するという構造です。  従って、- **逆順に**各層を1つずつ処理する`for`ループと、    - 層の中のノードを1つずつ処理する`for`ループの2段階構造が必要で、    - その中に「1つのノードにおける逆伝播の処理」を記述すればよいわけです。<jupyter_code>def back_prop(layers, weights, biases, y_true, cached_outs, cached_sums):
    """
    逆伝播を行う関数。
    - 引数：
    (layers, weights, biases)： モデルを指定する。
    y_true： 正解値（出力層のノードが複数ある場合もあるのでリスト値）。
    cached_outs： 順伝播で記録した活性化関数の出力値。予測値を含む。
    cached_sums： 順伝播で記録した線形和（Σ）値。
    - 戻り値：
    重みの勾配とバイアスの勾配を返す。
    """

    # ネットワーク全体で勾配を保持するためのリスト
    grads_w =[]  # 重みの勾配
    grads_b = []  # バイアスの勾配
    grads_x = []  # 入力の勾配

    layer_count = len(layers)
    layer_max_i = layer_count-1
    SKIP_INPUT_LAYER = 1
    PREV_LAYER = 1
    rng = range(SKIP_INPUT_LAYER, layer_count)  # 入力層以外の層インデックス
    for layer_i in reversed(rng):  # 各層を逆順に処理

        is_output_layer = (layer_i == layer_max_i)
        # 層ごとで勾配を保持するためのリスト
        layer_grads_w = []
        layer_grads_b = []
        layer_grads_x = []

        # （1）逆伝播していく誤差情報
        if is_output_layer:
            # 出力層（損失関数の偏微分係数）
            back_error = []  # 逆伝播していく誤差情報
            y_pred = cached_outs[layer_i]
            for output, target in zip(y_pred, y_true):
                loss_der = sseloss_der(output, target)  # 誤差情報
                back_error.append(loss_der)
        else:
            # 隠れ層（次の層への入力の偏微分係数）
            back_error = grads_x[-1]  # 最後に追加された入力の勾配

        node_sums = cached_sums[layer_i - SKIP_INPUT_LAYER]
        for node_i, node_sum in enumerate(node_sums):  # 各ノードを処理

            # （2）活性化関数を偏微分
            if is_output_layer:
                # 出力層（恒等関数の微分）
                active_der = identity_der(node_sum)
            else:
                # 隠れ層（シグモイド関数の微分）
                active_der = sigmoid_der(node_sum)

            # （3）線形和を重み／バイアス／入力で偏微分
            w = weights[layer_i - SKIP_INPUT_LAYER][node_i]
            b = biases[layer_i - SKIP_INPUT_LAYER][node_i]
            x = cached_outs[layer_i - PREV_LAYER]  # 前の層の出力＝今の層への入力
            sum_der_w = sum_der(x, w, b, with_respect_to='w')
            sum_der_b = sum_der(x, w, b, with_respect_to='b')
            sum_der_x = sum_der(x, w, b, with_respect_to='x')

            # （4）各重み／バイアス／各入力の勾配を計算
            delta = back_error[node_i] * active_der

            # バイアスは1つだけ
            grad_b = delta * sum_der_b
            layer_grads_b.append(grad_b)

            # 重みと入力は前の層のノードの数だけある
            node_grads_w = []
            for x_i, (each_dw, each_dx) in enumerate(zip(sum_der_w, sum_der_x)):
                # 重みは個別に取得する
                grad_w = delta * each_dw
                node_grads_w.append(grad_w)

                # 入力は各ノードから前のノードに接続する全ての入力を合計する
                # （※重み視点と入力視点ではエッジの並び方が違うので注意）
                grad_x = delta * each_dx
                if node_i == 0:
                    # 最初に、入力の勾配を作成
                    layer_grads_x.append(grad_x)
                else:
                    # その後は、その入力の勾配に合計していく
                    layer_grads_x[x_i] += grad_x
            layer_grads_w.append(node_grads_w)

        # 層ごとの勾配を、ネットワーク全体用のリストに格納
        grads_w.append(layer_grads_w)
        grads_b.append(layer_grads_b)
        grads_x.append(layer_grads_x)

    # 保持しておいた各勾配（※逆順で追加したので反転が必要）を戻り値で返す
    grads_w.reverse()
    grads_b.reverse()
    return (grads_w, grads_b)  # grads_xは最適化で不要なので返していない<jupyter_output><empty_output><jupyter_text>●逆伝播の実行例 以下のようなコードを書けば、順伝播から逆伝播までを続けて実行できます。<jupyter_code>x = [0.05, 0.1]
layers = [2, 2, 2]
weights = [
    [[0.15, 0.2], [0.25, 0.3]],
    [[0.4, 0.45], [0.5,0.55]]
]
biases = [[0.35, 0.35], [0.6, 0.6]]
model = (layers, weights, biases)
y_true = [0.01, 0.99]

# （1）順伝播の実行例
y_pred, cached_outs, cached_sums = forward_prop(*model, x, cache_mode=True)
print(f'y_pred={y_pred}')
print(f'cached_outs={cached_outs}')
print(f'cached_sums={cached_sums}')
# 出力例：
# y_pred=[1.10590596705977, 1.2249214040964653]
# cached_outs=[[0.05, 0.1], [0.5932699921071872, 0.596884378259767], [1.10590596705977, 1.2249214040964653]]
# cached_sums=[[0.3775, 0.39249999999999996], [1.10590596705977, 1.2249214040964653]]

# （2）逆伝播の実行例
grads_w, grads_b = back_prop(*model, y_true, cached_outs, cached_sums)
print(f'grads_w={grads_w}')
print(f'grads_b={grads_b}')
# 出力例：
# grads_w=[[[0.006706025259285303, 0.013412050518570607], [0.007487461943833829, 0.014974923887667657]], [[0.6501681244277691, 0.6541291517796395], [0.13937181955411934, 0.1402209162240302]]]
# grads_b=[[0.13412050518570606, 0.14974923887667657], [1.09590596705977, 0.23492140409646534]]<jupyter_output><empty_output><jupyter_text>■ステップ3. パラメーター（重みとバイアス）更新の実装 パラメーター（各重みと各バイアス）を更新する目的は、「**ニューラルネットのモデルを最適化すること**」です。下の図は「最適化の**参考イメージ**」です。※なお、本稿で説明するのは最も基礎的な**勾配降下法**（**Gradient Descent**）です。後述するSGD（確率的勾配降下法）もその一種で、他にはRMSPropやAdamなどより応用的な手法があります。SGD以外の場合は、重みパラメーターの更新方法も少し変わってきます。 ![図14　最適化の参考イメージ](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/03-optimizer/images/14.png)  ●1つのパラメーターの更新 1つの重み／バイアスのパラメーター更新をPythonコードで書くと以下のようになります。<jupyter_code># 取りあえず仮で、変数を定義して、コードが実行できるようにしておく
w_ij = 0.0  # 各重み
b_j = 0.0  # バイアス
grad_w_ij = 0.2  # 各重みの勾配
grad_b_j = 0.2  # バイアスの勾配
LEARNING_RATE = 0.1  # 学習率（lr）
lr = LEARNING_RATE
# ---ここまでは仮の実装。ここからが必要な実装---

w_ij = w_ij - lr * grad_w_ij  # 重みパラメーターの更新

b_j = b_j - lr * grad_b_j  # バイアスパラメーターの更新<jupyter_output><empty_output><jupyter_text>![重みパラメーター更新の計算式](https://raw.githubusercontent.com/isshiki/neural-network-by-code/main/03-optimizer/images/15.png)  ●パラメーター更新の処理全体の実装 ニューラルネットは、層があり、その中に複数のノードが存在するという構造ですので、- 各層を1つずつ処理するforループと  - 層の中のノードを1つずつ処理するforループの2段階構造が必要で    - その中に「1つのパラメーターの更新」を記述すればよいわけです。<jupyter_code>def update_params(layers, weights, biases, grads_w, grads_b, lr=0.1):
    """
    パラメーター（重みとバイアス）を更新する関数
    - 引数：
    (layers, weights, biases)： モデルを指定する。
    grads_w： 重みの勾配。
    grads_b： バイアスの勾配。
    lr： 学習率（learning rate）。最適化を進める量を調整する。
    - 戻り値：
    新しい重みとバイアスを返す。
    """

    # ネットワーク全体で勾配を保持するためのリスト
    new_weights = [] # 重み
    new_biases = [] # バイアス

    SKIP_INPUT_LAYER = 1
    for layer_i, layer in enumerate(layers):  # 各層を処理
        if layer_i == 0:
            continue  # 入力層はスキップ

        # 層ごとで勾配を保持するためのリスト
        layer_w = []
        layer_b = []

        for node_i in range(layer):  # 層の中の各ノードを処理
            b = biases[layer_i - SKIP_INPUT_LAYER][node_i]
            grad_b = grads_b[layer_i - SKIP_INPUT_LAYER][node_i]
            b = b - lr * grad_b  # バイアスパラメーターの更新
            layer_b.append(b)

            node_weights = weights[layer_i - SKIP_INPUT_LAYER][node_i]
            node_w = []
            for each_w_i, w in enumerate(node_weights):
                grad_w = grads_w[layer_i - SKIP_INPUT_LAYER][node_i][each_w_i]
                w = w - lr * grad_w  # 重みパラメーターの更新
                node_w.append(w)
            layer_w.append(node_w)

        new_weights.append(layer_w)
        new_biases.append(layer_b)
    
    return (new_weights, new_biases)<jupyter_output><empty_output><jupyter_text>●パラメーター更新の実行例 以下のようなコードを書けば、順伝播から逆伝播、パラメーター更新までを続けて実行できます。<jupyter_code>layers = [2, 2, 2]
weights = [
    [[0.15, 0.2], [0.25, 0.3]],
    [[0.4, 0.45], [0.5,0.55]]
]
biases = [[0.35, 0.35], [0.6, 0.6]]
model = (layers, weights, biases)

# 元の重み
print(f'old-weights={weights}')
print(f'old-biases={biases}' )
# old-weights=[[[0.15, 0.2], [0.25, 0.3]], [[0.4, 0.45], [0.5, 0.55]]]
# old-biases=[[0.35, 0.35], [0.6, 0.6]]

# （1）順伝播の実行例
x = [0.05, 0.1]
y_pred, cached_outs, cached_sums = forward_prop(*model, x, cache_mode=True)

# （2）逆伝播の実行例
y_true = [0.01, 0.99]
grads_w, grads_b = back_prop(*model, y_true, cached_outs, cached_sums)
print(f'grads_w={grads_w}')
print(f'grads_b={grads_b}')
# grads_w=[[[0.006706025259285303, 0.013412050518570607], [0.007487461943833829, 0.014974923887667657]], [[0.6501681244277691, 0.6541291517796395], [0.13937181955411934, 0.1402209162240302]]]
# grads_b=[[0.13412050518570606, 0.14974923887667657], [1.09590596705977, 0.23492140409646534]]

# （3）パラメーター更新の実行例
LEARNING_RATE = 0.1 # 学習率（lr）
weights, biases = update_params(*model, grads_w, grads_b, lr=LEARNING_RATE)

# 更新後の新しい重み
print(f'new-weights={weights}')
print(f'new-biases={biases}')
# new-weights=[[[0.14932939747407145, 0.19865879494814295], [0.2492512538056166, 0.2985025076112332]], [[0.3349831875572231, 0.3845870848220361], [0.48606281804458806, 0.5359779083775971]]]
# new-biases=[[0.3365879494814294, 0.33502507611233234], [0.490409403294023, 0.5765078595903534]]<jupyter_output><empty_output><jupyter_text>■3つのステップを呼び出す最適化処理の実装  ●最適化処理：学習方法と勾配降下法 代表的な学習方法を簡単にまとめておきます。- **オンライン学習**（**Online training**）： データ1件ずつ訓練していくこと- **ミニバッチ学習**（**Mini-batch training**）： 小さなまとまりのデータごとに訓練していくこと- **バッチ学習**（**Batch training**）： データ全件で訓練していくこと 学習方法ごとに、勾配降下法をまとめると以下のようになります。- **SGD**（**Stochastic Gradient Descent**）： オンライン学習- **ミニバッチSGD**（**Mini-batch SGD**）： ミニバッチ学習。単に**ミニバッチ勾配降下法**（**Mini-batch Gradient Descent**）とも呼ぶ- **最急降下法**（**Steepest Descent**）： バッチ学習。**バッチ勾配降下法**（**Batch Gradient Descent**）とも呼ぶ コード内容も簡単なので少し難易度を上げて、あえて全ての学習方法に対応できる実装コードにしてみます。  ●最適化の処理全体の実装 訓練処理では、エポック（＝全データ分で1回の訓練）があり、その中にイテレーション（＝バッチサイズごとでのパラメーターの更新）が存在するという構造ですので、- エポックを1回ずつ処理するforループと  - その中にデータを1件ずつ処理するforループの2段階構造を用意し    - その中に「ステップ①順伝播」「ステップ②逆伝播」と    - イテレーションごとに「ステップ③パラメーターの更新」を記述するようにします（※あくまで筆者による実装方針の例です）。 階層が深くなる上にコードの行数が少し長いので、説明の都合上、上の箇条書きの前半2行を`train()`親関数、後半2行を`optimize()`子関数、という親子関係の2つの関数に分けて記述します。※1つの関数として実装した方がシンプルになって見通しもよくなるので、本来であればそうした方がよいと思います。<jupyter_code>import random

# 取りあえず仮で、空の関数を定義して、コードが実行できるようにしておく
def optimize(model, x, y, data_i, last_i, batch_i, batch_size, acm_g, lr=0.1):
    " モデルを最適化する関数（子関数）。"
    loss = 0.1
    return model, loss, batch_i, acm_g

# ---ここまでは仮の実装。ここからが必要な実装---

def train(model, x, y, batch_size=32, epochs=10, lr=0.1, verbose=10):
    """
    モデルの訓練を行う関数（親関数）。
    - 引数：
    model： モデルをタプル「(layers, weights, biases)」で指定する。
    x： 訓練データ（各データが行、各特徴量が列の、2次元リスト値）。
    y： 訓練ラベル（各データが行、各正解値が列の、2次元リスト値）。
    batch_size： バッチサイズ。何件のデータをまとめて処理するか。
    epochs： エポック数。全データ分で何回、訓練するか。
    lr： 学習率（learning rate）。最適化を進める量を調整する。
    verbose： 訓練状況を何エポックおきに出力するか。
    - 戻り値：
    損失値の履歴を返す。これを使って損失値の推移グラフが描ける。
    """
    loss_history = []  # 損失値の履歴

    data_size = len(y)  # 訓練データ数
    data_indexes = range(data_size)  # 訓練データのインデックス

    # 各エポックを処理
    for epoch_i in range(1, epochs + 1):  # 経過表示用に1スタート

        acm_loss = 0  # 損失値を蓄積（accumulate）していく

        # 訓練データのインデックスをシャッフル（ランダムサンプリング）
        random_indexes = random.sample(data_indexes, data_size)
        last_i = random_indexes[-1]  # 最後の訓練データのインデックス

        # 親関数で管理すべき変数
        acm_g = (None, None)  # 重み／バイアスの勾配を蓄積していくため
        batch_i = 0  # バッチ番号をインクリメントしていくため

        # 訓練データを1件1件処理していく
        for data_i in random_indexes:

            # 親子に分割したうちの子関数を呼び出す
            model, loss, batch_i, acm_g = optimize(
                model, x, y, data_i, last_i, batch_i, batch_size, acm_g, lr)

            acm_loss += loss  # 損失値を蓄積

        # エポックごとに損失値を計算。今回の実装では「平均」する
        layers = model[0]  # レイヤー構造
        out_count = layers[-1]  # 出力層のノード数
        # 「訓練データ数（イテレーション数×バッチサイズ）×出力ノード数」で平均
        epoch_loss = acm_loss / (data_size * out_count)

        # 訓練状況を出力
        if verbose != 0 and \
            (epoch_i % verbose == 0 or epoch_i == 1 or epoch_i == EPOCHS):
            print(f'[Epoch {epoch_i}/{EPOCHS}] train_loss: {epoch_loss}')

        loss_history.append(epoch_loss)  # 損失値の履歴として保存

    return model, loss_history


# サンプル実行用の仮のモデルとデータ
layers = [2, 2, 2]
weights = [
    [[0.15, 0.2], [0.25, 0.3]],
    [[0.4, 0.45], [0.5,0.55]]
]
biases = [[0.35, 0.35], [0.6, 0.6]]
model = (layers, weights, biases)
x = [[0.05, 0.1]]
y = [[0.01, 0.99]]

# モデルを訓練する
BATCH_SIZE = 2  # バッチサイズ
EPOCHS = 1  # エポック数
LEARNING_RATE = 0.02 # 学習率（lr）
model, loss_history = train(model, x, y, BATCH_SIZE, EPOCHS, LEARNING_RATE)
# 出力例：
# [Epoch 1/1] train_loss: 0.05
def accumulate(list1, list2):
    "2つのリストの値を足し算する関数。"
    new_list = []
    for item1, item2 in zip(list1, list2):
        if isinstance(item1, list):
            child_list = accumulate(item1, item2)
            new_list.append(child_list)
        else:
            new_list.append(item1 + item2)
    return new_list

# （NumPy利用バージョン）
# import numpy as np
# def accumulate(list1, list2):
#     "2つのリストの値を足し算する関数。"
#     new_list = []
#     for item1, item2 in zip(list1, list2):
#         # ※全体の重み勾配は行数と列数が同じではないので層ごとに処理する必要がある。
#         np_sum = np.array(item1) + np.array(item2)  # NumPyなら行列データをまとめて処理できる
#         new_list.append(np_sum.tolist())
#     return new_list
def mean_element(list1, data_count):
    "1つのリストの値をデータ数で平均する関数。"
    new_list = []
    for item1 in list1:
        if isinstance(item1, list):
            child_list = mean_element(item1, data_count)
            new_list.append(child_list)
        else:
            new_list.append(item1 / data_count)
    return new_list

# （NumPy利用バージョン）
# import numpy as np
# def mean_element(list1, data_count):
#     "1つのリストの値をデータ数で平均する関数。"
#     new_list = []
#     for item1 in list1:
#         # ※全体の重み勾配は行数と列数が同じではないので層ごとに処理する必要がある。
#         np_mean = np.array(item1) / data_count  # NumPyなら行列データをまとめて処理できる
#         new_list.append(np_mean.tolist())
#     return new_list
def optimize(model, x, y, data_i, last_i, batch_i, batch_size, acm_g, lr=0.1):
    "train()親関数から呼ばれる、最適化のための子関数。"

    layers = model[0]  # レイヤー構造
    each_x = x[data_i]  # 1件分の訓練データ
    y_true = y[data_i]  # 1件分の正解値

    # ステップ（1）順伝播
    y_pred, outs, sums = forward_prop(*model, each_x, cache_mode=True)

    # ステップ（2）逆伝播
    gw, gb = back_prop(*model, y_true, outs, sums)

    # 各勾配を蓄積（accumulate）していく
    if batch_i == 0:
        acm_gw = gw
        acm_gb = gb
    else:
        acm_gw = accumulate(acm_g[0], gw)
        acm_gb = accumulate(acm_g[1], gb)
    batch_i += 1  # バッチ番号をカウントアップ＝現在のバッチ数

    # 訓練状況を評価するために、損失値を取得
    loss = 0.0
    for output, target in zip(y_pred, y_true):
        loss += sseloss(output, target)

    # バッチサイズごとで後続の処理に進む
    if batch_i % BATCH_SIZE != 0 and data_i != last_i:
        return model, loss, batch_i, (acm_gw, acm_gb)  # バッチ内のデータごと

    layers = model[0]  # レイヤー構造
    out_count = layers[-1]  # 出力層のノード数

    # 平均二乗誤差なら平均する（損失関数によって異なる）
    grads_w = mean_element(acm_gw, batch_i * out_count)  # 「バッチサイズ ×
    grads_b = mean_element(acm_gb, batch_i * out_count)  #   出力ノード数」で平均
    batch_i = 0  # バッチ番号を初期化して次のイテレーションに備える

    # ステップ（3）パラメーター（重みとバイアス）の更新
    weights, biases = update_params(*model, grads_w, grads_b, lr)

    # モデルをアップデート（＝最適化）
    model = (layers, weights, biases)

    return model, loss, batch_i, (acm_gw, acm_gb)  # イテレーションごと


# サンプル実行
model, loss_history = train(model, x, y, BATCH_SIZE, EPOCHS, LEARNING_RATE)
# 出力例：
# [Epoch 1/1] train_loss: 0.31404948868496607<jupyter_output><empty_output><jupyter_text>■回帰問題を解くデモ 特徴量（入力データ）は$x_1$（X軸）と$x_2$（Y軸）の座標です。その座標点における色、具体的にはオレンジ色（**-1**）～灰色（**0**）～青色（**1**）を予測する回帰問題となります。 まず訓練データを用意します。このデモの訓練データは、「[回帰問題をディープラーニング（基本のDNN）で解こう](https://atmarkit.itmedia.co.jp/ait/articles/2005/25/news011.html)」でも使っているライブラリー「playground-data」の平面（Plain）データセットです。<jupyter_code>!pip install playground-data
# playground-dataライブラリのplygdataパッケージを「pg」という別名でインポート
import plygdata as pg

# 問題種別で「分類（Classification）」を選択し、
# データ種別で「2つのガウシアンデータ（TwoGaussData）」を選択する場合の、
# 設定値を定数として定義
PROBLEM_DATA_TYPE = pg.DatasetType.RegressPlane

# 各種設定を定数として定義
TRAINING_DATA_RATIO = 0.5  # データの何％を訓練【Training】用に？ (残りは精度検証【Validation】用) ： 50％
DATA_NOISE = 0.0           # ノイズ： 0％

# 定義済みの定数を引数に指定して、データを生成する
data_list = pg.generate_data(PROBLEM_DATA_TYPE, DATA_NOISE)

# データを「訓練用」と「精度検証用」を指定の比率で分割し、さらにそれぞれを「データ（X）」と「教師ラベル（y）」に分ける
X_train, y_train, _, _ = pg.split_data(data_list, training_size=TRAINING_DATA_RATIO)

# それぞれ5件ずつ出力
print('X_train:'); print(X_train[:5])
print('y_train:'); print(y_train[:5])<jupyter_output><empty_output><jupyter_text>次に、モデルを定義します。<jupyter_code>layers = [2, 3, 1]
weights = [
    [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]],
    [[0.0, 0.0, 0.0]]
]
biases = [
    [0.0, 0.0, 0.0],  # hidden1
    [0.0]  # output
]
model = (layers, weights, biases)<jupyter_output><empty_output><jupyter_text>訓練「前」のモデルによる予測状態を図示します。<jupyter_code># `draw_decision_boundary()`関数がクラス内の`predict()`メソッドを呼び出す仕様のため
class MyModel:
    def __init__(self, l, w, b):
        self.layers = l
        self.weights = w
        self.biases = b

    def predict(self, x, batch_size=1, verbose=False):
        probability = []
        for each_x in x:
            y = forward_prop(self.layers, self.weights, self.biases, each_x)
            probability.append(y[0])
        return probability

# 出力のグラフ表示
trained_model = MyModel(*model)
fig, ax = pg.plot_points_with_playground_style(X_train, y_train, None, None, figsize = (6, 6), dpi = 100)
pg.draw_decision_boundary(fig, ax, trained_model=trained_model, discretize=False)<jupyter_output><empty_output><jupyter_text>それぞれの丸い点の座標は、訓練データ1件1件の特徴量を表します。その点の色が正解ラベルです。例えば左下の座標点でれば、色はオレンジ色、つまり**-1.0**に近い値が正解となります。右上が青色、つまり**1.0**に近い値が正解です。モデルによる予測値は、背景色として描画されています。上の図は全面が灰色です。これは、どの座標を入力しても、**0.0**が予測されることを意味します。これをニューラルネットで学習することで、オレンジ色の座標点の背景色はオレンジ色に、青色の座標点の背景色は青色に描画されるようにします。 最後に、訓練処理の`train()`関数を呼び出すだけです。<jupyter_code>import matplotlib.pyplot as plt

BATCH_SIZE = 4   # バッチサイズ
EPOCHS = 100     # エポック数
LERNING_RATE = 0.02  # 学習係数

model, loss_history = train(model, X_train, y_train, BATCH_SIZE, EPOCHS, LEARNING_RATE)

# 学習結果（損失）のグラフを描画
epochs = len(loss_history)
plt.plot(range(1, epochs + 1), loss_history, marker='.', label='loss (Training data)')
plt.legend(loc='best')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()<jupyter_output><empty_output><jupyter_text>重みやバイアスはモデル（タプル型オブジェクト）の中に格納されています。<jupyter_code>print(f'weights={model[1]}')
print(f'biases={model[2]}')<jupyter_output><empty_output><jupyter_text>訓練「後」のモデルによる予測状態を図示します。<jupyter_code># 出力のグラフ表示
trained_model = MyModel(*model)
fig, ax = pg.plot_points_with_playground_style(X_train, y_train, None, None, figsize = (6, 6), dpi = 100)
pg.draw_decision_boundary(fig, ax, trained_model=trained_model, discretize=False)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.14361108008488774, 'fraction_numerical': 0.05174243270412152, 'mean_word_length': 4.158144894137981, 'pattern_counts': {'":': 0, '<': 126, '<?xml version=': 0, '>': 125, 'https://': 19, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '31', 'dir': 'jupyter-structured-clean-dedup', 'id': '585962', 'license': 'Apache-2.0', 'n_tokens_mistral': 23223, 'n_tokens_neox': 22021, 'n_words': 2024, 'path': '01-forward-prop/nn_from_scratch_without_numpy.ipynb', 'repo_name': 'isshiki/neural-network-by-code', 'size': '35812'}