Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

ニューラルネットワーク

Open In Colab

ニューラルネットワークは、人間の脳に似た層状構造で相互接続されたノードやニューロンを使用するの計算モデルです。

ニューラルネットワークは、画像認識、自然言語処理、音声認識など、さまざまな領域で広く利用されています。特に、大量のデータと計算能力が利用可能になった近年、ディープニューラルネットワーク(DNN)の研究や応用が急速に進展しています。

1ニューラルネットワークの構造

1.1パーセプトロン

パーセプトロンとは、複数の入力を受け取り、重み付けして、1つの信号を出力するアルゴリズムです。

例えば,x1x_1x2x_2の2つの入力を受け取り、yを出力するパーセプトロンを考えます。

  • w1w_1w2w_2は各入力の「重み」を表すパラメータで、各入力の重要性をコントロールします。

  • bbはバイアス

パーセプトロンの「○」で表されている部分は、ニューロンやノードと呼びます。

パーセプトロンの「○」で表されている部分は、ニューロンやノードと呼びます。

1.2活性化関数

活性化関数はニューラルネットワークの各層において、入力データに対して非線形性を導入するために使用される関数です。

例えば、関数の入力(パーセプトロンだと重み付き和)が0以下のとき0を、0より大きいとき1を出力することが考えます。

y={0(w1x1+w2x2+b0)1(w1x1+w2x2+b>0)y = \begin{cases} 0 \quad (w_1 x_1 + w_2 x_2 + b \leq 0) \\ 1 \quad (w_1 x_1 + w_2 x_2 + b > 0) \end{cases}

出力に関する計算数式を分解すると、

y=h(a)y = h(a)
h(a)={0(a0)1(a>0)h(a) = \begin{cases} 0 \quad (a \leq 0) \\ 1 \quad (a > 0) \end{cases}

で書けます。つまり、入力の重み付き和の結果がaaというノードになり、そして活性化関数h()h()によってyyという出力が計算されます。

活性化関数があるパーセプトロン

活性化関数があるパーセプトロン

活性化関数の主な役割は、入力の加重和に非線形な変換を加えることで、ニューラルネットワークが複雑なパターンを学習できるようにすることです。

例えば、線形変換のみで下図右の丸で表される観測データからxxyyの関係を近似した場合、点線のような直線が得られたとします。これでは、一部のデータについてはあまりよく当てはまっていないのが分かります。

しかし、もし図右の実線のような曲線を表現することができれば、両者の関係をより適切に表現することができます。

活性化関数にはいくつか種類があり、異なる特性や用途を持っています。

  • シグモイド関数

    • 任意の値を0から1に変換します

  • ReLU

    • 負の入力は0として、0もしくは正の入力はそのまま出力

活性化関数の種類

活性化関数の種類

1.3ニューラルネットワークの仕組み

ニューラルネットワークの仕組みは下の図で表さます。左側から、最初の層を入力層 (input layer)、最後の層を出力層 (output layer)といいます。

その間にある層は中間層 (intermediate layer) もしくは隠れ層 (hidden layer) といいます。中間層において、層の数を増やすことによって、ディープニューラルネットワークを実現することができます。

ニューラルネットワークは、層から層へ、値を変換していきます。 そのため、ニューラルネットワークとはこの変換がいくつも連なってできる一つの大きな関数だと考えることができます。 従って、基本的には、入力を受け取って、何か出力を返すものです。 そして、どのようなデータを入力し、どのような出力を作りたいかによって、入力層と出力層のノード数が決定されます。

2ニューラルネットワークの計算

それでは、下図に示す3層ニューラルネットワークを例として、入力から出力への計算のについて解説を行います。

2.1記号の説明

ニューラルネットワークの計算を説明するにあたって、導入される記号の定義から始めます。

入力層のx1x_1x2x_2ニューロンから、次層のニューロンa1(1)a_1^{(1)}への信号伝達を見ていきます。

  • w12(1)w_{12}^{(1)} は前層の2番目のニューロン(x2x_2)から次層の1番目のニューロン(a1(1)a_1^{(1)})への重みであることを意味します。

    • 右上(1)(1)は第1層の重みということ意味します

    • 右下12ような数字の並びは、次層のニューロン(1)と前層のニューロンのインデックス番号(2)から構成されます

  • a1(1)a_1^{(1)}は第11番目のニューロンであることを意味します。

    • 右上(1)(1)は第1層のニューロンということ意味します

    • 右下11番目のニューロンということ意味します

2.2各層における信号伝達

まず、入力層から「第1層の1番目のニューロン」への信号伝達を見ていきます。ここでは。バイアス項も追加し、a1(1)a_1^{(1)}を以下の数式で計算します。

a1(1)=w11(1)x1+w12(1)x2+b1(1)a_1^{(1)}= w_{11}^{(1)}x_{1} + w_{12}^{(1)}x_{2} + b_1^{(1)}

同じ形で、第1層におけるすべでのニューロンの計算式を書けます。

{a1(1)=w11(1)x1+w12(1)x2+b1(1)a2(1)=w21(1)x1+w22(1)x2+b2(1)a3(1)=w31(1)x1+w32(1)x2+b3(1)\begin{split}\begin{cases} a_1^{(1)} = w_{11}^{(1)}x_{1} + w_{12}^{(1)}x_{2} + b_1^{(1)} \\ a_2^{(1)} = w_{21}^{(1)}x_{1} + w_{22}^{(1)}x_{2} + b_2^{(1)} \\ a_3^{(1)} = w_{31}^{(1)}x_{1} + w_{32}^{(1)}x_{2} + b_3^{(1)} \end{cases}\end{split}

行列で第1層におけるニューロンの計算式をまとめて表すことができます。

  • 入力 X=(x1x2)\mathbf{X}=\begin{pmatrix} x_1 & x_2 \end{pmatrix}

  • バイアス B=(b1(1)b2(1)b3(1))\mathbf{B} = \begin{pmatrix} b_{1}^{(1)} & b_{2}^{(1)} & b_{3}^{(1)} \end{pmatrix}

  • 重み

W=(w11(1)w21(1)w31(1)w12(1)w22(1)w32(1))\begin{split} \mathbf{W} = \begin{pmatrix} w_{11}^{(1)} & w_{21}^{(1)} & w_{31}^{(1)} \\ w_{12}^{(1)} & w_{22}^{(1)} & w_{32}^{(1)} \end{pmatrix}\end{split}
  • 入力・バイアスと重みの総和: A=(a1(1)a2(1)a3(1))\mathbf{A} = \begin{pmatrix} a_1^{(1)} & a_2^{(1)} & a_3^{(1)} \end{pmatrix}

A(1)=XW(1)+B(1)\mathbf{A}^{(1)} = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{B}^{(1)}

さらに、活性化関数を導入します。入力・バイアスと重みの総和をaaで表し、活性化関数h()h()による変換された結果をzzで表すことにします。

2.3数値を見ながら計算の流れを確認

それでは、NumPyの多次元配列を使って、入力 x1x_1,x2x_2,x3x_3から出力が計算される過程を確認してみましょう。入力、重み、バイアスは適当な値を設定します。

import numpy as np
X = np.array([1.0, 0.5])
W1 = np.array([[0.1, 0.3, 0.5],[0.2, 0.4, 0.6]])
B1 = np.array([0.1, 0.2, 0.3])
print(r"入力の形状: {}".format(X.shape))
print(r"重みの形状: {}".format(W1.shape))
print(r"バイアスの形状: {}".format(B1.shape))
入力の形状: (2,)
重みの形状: (2, 3)
バイアスの形状: (3,)

第一層隠れ層で重み付きとバイアスの総和を計算し、活性化関数で変換された結果を返します。

A1 = np.dot(X, W1) + B1
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
Z1 = sigmoid(A1)

続いて、同じ形で第1層から第2層目への信号伝達を行います。

W2 = np.array([[0.1, 0.4],[0.2, 0.5],[0.3, 0.6]])
B2 = np.array([0.1, 0.2])
A2 = np.dot(Z1, W2) + B2
Z2 = sigmoid(A2)

最後に、第2層から出力層への信号を行います。出力層の活性化関数は、恒等関数を用います。

W3 = np.array([[0.1, 0.3],[0.2, 0.4]])
B3 = np.array([0.1, 0.2])
A3 = np.dot(Z2, W3) + B3 # Y = A3

2.4出力層の設計

ニューラルネットワークは、分類問題と回帰問題の両方に用いることができます。ただし、分類問題と回帰問題のどちらに用いるかで、出力層の活性化関数を変更する必要があります。

  • 回帰問題

    • モデルの出力は連続的な実数値でなければなりません。そのため、出力層に活性化関数を使用しないか、出力の範囲を制限しない活性化関数を使用します。

  • 分類問題

    • 分類問題では、モデルの出力を特定のクラスに分類するため、出力は確率として解釈できる必要があります。一般的には クラス数と同じだけのノードを出力層に用意しておき、各ノードがあるクラスに入力が属する確率を表すようにします。 このため、全出力ノードの値の合計が1になるよう正規化します。 これには、要素ごとに適用される活性化関数ではなく、層ごとに活性値を計算する別の関数を用いる必要があります。 そのような目的に使用される代表的な関数には、ソフトマックス関数があります。

2.4.1ソフトマックス関数

ソフトマックス関数は複数値からなるベクトルを入力し、それを正規化したベクトルを出力します。ソフトマックス関数は、次の式で定義されます。

yk=exp(ak)k=0K1exp(ak)y_k = \frac{\exp(a_k)}{\sum_{k'=0}^{K-1} \exp(a_{k'})}

KK個の要素a=(a0,a1,,aK1)\mathbf{a} = (a_0, a_1, \cdots, a_{K-1})を入力して、0yk10 \leq y_k \leq 1k=0K1yk=1\sum_{k=0}^{K-1} y_k = 1となるy=(y0,y1,,yK1)\mathbf{y} = (y_0, y_1, \cdots, y_{K-1})を出力します。つまり、ソフトマックス関数を適用することで、各成分は区間 (0,1)(0, 1) に収まり、全ての成分の和が 1 になるため、「確率」として解釈できるようになります。

実装の際、指数関数の計算のため容易に大きな値になり、計算結果はinfが返ってきますので、数値が不安定になってしまう「オーバーフロー」問題を対応するため。入力の最大値を引くことで、正しく計算するようにする方法が採用されています。

import numpy as np
def softmax(x):
    c = np.max(x)
    exp_x = np.exp(x - c)
    sum_exp_x = np.sum(exp_x)
    return exp_x / sum_exp_x
print("活性化関数に適用する前に: {}".format(A3))
print("最終出力: {}".format(softmax(A3)))
活性化関数に適用する前に: [0.31682708 0.69627909]
最終出力: [0.40625907 0.59374093]

3ニューラルネットワークの学習

重回帰分析では、最小二乗法などの推定方法で行列計算や微分方程式を用いて解を導出することができます。つまり、実際の数値を使うことなく変数のまま、解(最適なパラメータ)を求めることができました。このように、変数のままで解を求めることを解析的に解くと言い、その答えのことを解析解 (analytical solution) と呼びます。

しかし、ニューラルネットワークで表現されるような複雑な関数の場合、パラメータの数は数億に及ぶこともありますので、最適解を解析的に解くことはほとんどの場合困難です。そのため、別の方法を考える必要があります。具体的には、解析的に解く方法に対し、計算機を使って繰り返し数値計算を行って解を求めることを数値的に解くといい、求まった解は数値解 (numerical solution) と呼ばれます。

ニューラルネットワークでは、基本的に数値的な手法によって最適なパラメータを求めます。

3.1損失関数

損失関数(Loss function)とは、「正解値」と、モデルによる出力された「予測値」とのズレの大きさ(これを「Loss:損失」と呼ぶ)を計算するための関数です。損失関数の値は、学習アルゴリズムがモデルのパラメータを調整する際の指標となります。

3.1.1平均二乗誤差

平均二乗誤差 (mean squared error) は、回帰問題を解きたい場合によく用いられる目的関数です。 重回帰分析の解説中に紹介した二乗和誤差と似ていますが、各データ点における誤差の総和をとるだけでなく、それをデータ数で割って、誤差の平均値を計算している点が異なります。

L=1Nn=1N(tnyn)2L = \frac{1}{N} \sum_{n=1}^N (t_n - y_n)^2

ここで、NNはサンプルサイズ、yny_nnn個目のデータに対するニューラルネットワークの出力値、tnt_nnn個目のデータに対する望ましい正解の値です。

3.1.2交差エントロピー

交差エントロピー (cross entropy) は、分類問題を解きたい際によく用いられる目的関数です。

例として、KKクラスの分類問題を考えてみましょう。 ある入力xxが与えられたとき、ニューラルネットワークの出力層にKK個のノードがあり、それぞれがこの入力がkk番目のクラスに属する確率

yk=p(y=kx)y_k = p(y=k|x)

を表しているとします。 これは、入力xxが与えられたという条件のもとで、予測クラスを意味するyykkであるような確率、を表す条件付き確率です。

ここで、xxが所属するクラスの正解が、

t=[t1t2tK]T{\bf t} = \begin{bmatrix} t_1 & t_2 & \dots & t_K \end{bmatrix}^{\rm T}

というベクトルで与えられているとします。 ただし、このベクトルはtk(k=1,2,...,K)t_k (k=1,2,...,K) のいずれか一つだけが1であり、それ以外は0であるようなベクトルであるとします。

そして、この一つだけ値が1となっている要素は、その要素のインデックスに対応したクラスが正解であることを意味します。

以上を用いて、交差エントロピーは以下のように定義されます。

k=1Ktklogyk- \sum_{k=1}^{K}t_{k}\log y_{k}

これは、tkt_kk=1,2,...,Kk=1,2,...,K のうち正解クラスである一つのkkの値でだけ1となるので、正解クラスであるようなkkでのlogyk\log y_{k}を取り出して1−1を掛けているのと同じです。 また、NN個すべてのサンプルを考慮すると、交差エントロピーは以下になります

L=n=1Nk=1Ktn,klogyn,kL = - \sum_{n=1}^{N} \sum_{k=1}^{K}t_{n, k}\log y_{n, k}
Average loss

Average loss

課題

3クラス分類問題を考えます。

予測はy=(0.1,0.2,0.3)y=(0.1,0.2,0.3)、真のラベルはt=(0,0,1)t=(0,0,1)の場合、交差エントロピーの計算式を書いてください。

t = np.array([0, 0, 1])
y = np.array([0.1, 0.2, 0.5])

3.2損失関数の最適化

3.2.1勾配法

下の図は,パラメータwwを変化させた際の損失関数LLの値を表しています。損失関数の値を最小にするようなパラメータの値を求めることで、ニューラルネットワークを訓練します。ただ、実際のニューラルネットワークの目的関数は、多次元で、かつもっと複雑な形をしていることがほとんどです。 そこで、勾配を利用して関数の最小値を探す勾配法がよく用いられます。

勾配は、各地点における関数の傾きであり、関数の値が最も急速に変化する方向と大きさを示します。

今はLLの値を小さくしたいわけです。勾配の反対方向に進むことで関数の値を最も減らせることができますので、勾配の情報を手がかりに、できるだけ小さな値となる関数の場所を探します。

損失を求めるまでの計算を1つの関数とみなして、重みの勾配LW\frac{\partial L}{\partial \mathbf{W}}ととバイアスの勾配Lb\frac{\partial L}{\partial \mathbf{b}}を求めます。各要素は、それぞれパラメータの対応する要素の偏微分です。各パラメータの勾配LW\frac{\partial L}{\partial \mathbf{W}}Lb\frac{\partial L}{\partial \mathbf{b}}を用いて、勾配降下法によりパラメータW, b\mathbf{W},\ \mathbf{b}を更新します。

W(new)=WηLWb(new)=bηLb\begin{aligned} \mathbf{W}^{(\mathrm{new})} &= \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}} \\ \mathbf{b}^{(\mathrm{new})} &= \mathbf{b} - \eta \frac{\partial L}{\partial \mathbf{b}} \end{aligned}

η\etaは学習率と言います。1回の学習で、どれだけパラメータを更新するか、ということを決めます。

3.2.2勾配下降法の実装

3.2.2.1f(x)=x2f(x)=x^2に対する最適化
import numpy as np
import matplotlib.pyplot as plt

# 関数とその勾配
def function_f(x):
    return x ** 2

def numerical_gradient(f, x, h=1e-5):
    return (f(x + h) - f(x - h)) / (2 * h)
def gradient_descent(initial_x, learning_rate, num_iterations):
    x = initial_x
    x_history = [x]
    
    for i in range(num_iterations):
        grad = numerical_gradient(function_f, x)
        x = x - learning_rate * grad
        x_history.append(x)
        if i % 10 == 0:
            print("Iteration {}: x = {}, f(x) = {}".format(i, x, function_f(x)))
    
    return x_history
# パラメータ設定
initial_x = 5.0
learning_rate = 0.1
num_iterations = 100
x_history = gradient_descent(initial_x, learning_rate, num_iterations)
Iteration 0: x = 4.000000000037858, f(x) = 16.00000000030286
Iteration 10: x = 0.42949672960284735, f(x) = 0.18446744073954138
Iteration 20: x = 0.04611686018453473, f(x) = 0.0021267647932799246
Iteration 30: x = 0.004951760157169053, f(x) = 2.4519928654126888e-05
Iteration 40: x = 0.0005316911983169366, f(x) = 2.826955303677e-07
Iteration 50: x = 5.7089907708557185e-05, f(x) = 3.259257562171577e-09
Iteration 60: x = 6.1299821634977855e-06, f(x) = 3.757668132480099e-11
Iteration 70: x = 6.582018229321577e-07, f(x) = 4.3322963971121544e-13
Iteration 80: x = 7.067388259152989e-08, f(x) = 4.994797680561351e-15
Iteration 90: x = 7.588550360298899e-09, f(x) = 5.758609657079255e-17
# プロット
x = np.linspace(-initial_x-1, initial_x+1, 400)
y = function_f(x)

plt.figure(figsize=(8,5))
plt.plot(x, y, '-b', label='$f(x) = x^2$')
plt.scatter(x_history, [function_f(i) for i in x_history], c='red', label='Gradient Descent Steps')
plt.title('Gradient Descent Visualization')
plt.xlabel('$x$')
plt.ylabel('$f(x)$')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
<Figure size 800x500 with 1 Axes>
課題

勾配法でf(x)=2x210x80f(x)=2x^2-10x-80の最小値を求めます。

  • 勾配降下法のアルゴリズムを実装する。

  • アルゴリズムを使って関数の最小値を求める。

3.2.2.2Example: f(x0,x1)=x02+x12f(x_0,x_1)=x_0^2+x_1^2に対する最適化

今度は、変数が複数(2つ)あるの関数に対する最適化を実装してみます。複数の変数からなる関数の微分は偏微分といいます。

偏微分の場合、複数ある変数の中でターゲットとする変数を一つに絞り、他の変数はある値に固定します。

# 関数定義
def function_f(x):
    return x[0]**2 + x[1]**2
# 勾配
def numerical_gradient(f, x, h = 1e-4):
    grad = np.zeros_like(x)
    # x の各成分について順番にループを回し、数値的に勾配を計算します。
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = tmp_val + h
        fxh1 = f(x) # f(x+h)

        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)

        grad[idx] = (fxh1 - fxh2) / (2*h)

        x[idx] = tmp_val 
        it.iternext()   

    return grad

def gradient_descent(f, init_x, lr=0.01, num_iterations=100):
    x = init_x
    x_history = [x.copy()]

    for i in range(num_iterations):
        grad = numerical_gradient(f, x)
        x -= lr * grad
        x_history.append(x.copy())

    return np.array(x_history)

numerical_gradientちう関数で、配列の各要素に対して数値微分を求めます。例えば、点(3,4)(-3,4)での勾配を求めてみます。

勾配が示す方向は、各場所において関数の値を最も減らす方向であり、その方向に晋ことで関数の値を最も減らせることができます。つまり、勾配の情報を手がかりに、進む方向を決めるべきでしょう。

# 勾配の計算
numerical_gradient(function_f, np.array([-3.0, 4.0]))
array([-6., 8.])
# Parameters
init_x = np.array([-3.0, 4.0])
lr = 0.1
num_iterations = 20

# Run gradient descent
x_history = gradient_descent(function_f, init_x, lr, num_iterations)

# Generate mesh data for visualization
x = np.linspace(-4.5, 4.5, 200)
y = np.linspace(-4.5, 4.5, 200)
X, Y = np.meshgrid(x, y)
Z = X**2 + Y**2

fig = plt.figure(figsize=(15, 8))
# 1st subplot: 3D plot
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot_surface(X, Y, Z, cmap='viridis', alpha=0.6)
ax1.contour(X, Y, Z, zdir='z', offset=0, cmap='viridis', linestyles='dashed')
Z_history = np.array([function_f(x) for x in x_history])
ax1.plot(x_history[:, 0], x_history[:, 1], Z_history, 'o-', color='red')
for i in range(1, len(x_history)):
    ax1.quiver(x_history[i-1][0], x_history[i-1][1], Z_history[i-1], 
               x_history[i][0]-x_history[i-1][0], 
               x_history[i][1]-x_history[i-1][1], 
               Z_history[i]-Z_history[i-1], 
               color="blue", arrow_length_ratio=0.05)
ax1.set_xlabel("$X_0$")
ax1.set_ylabel("$X_1$")
ax1.set_zlabel("$f(X_0, X_1)$")
ax1.set_title("3D Visualization")

# 2nd subplot: 2D contour plot
ax2 = fig.add_subplot(122)
contour = ax2.contour(X, Y, Z, levels=10, colors='black', linestyles='dashed')
ax2.clabel(contour)
for i in range(1, len(x_history)):
    ax2.quiver(x_history[i-1][0], x_history[i-1][1], 
               x_history[i][0]-x_history[i-1][0], 
               x_history[i][1]-x_history[i-1][1], 
               angles="xy", scale_units="xy", scale=1, color="blue")
ax2.plot(x_history[:, 0], x_history[:, 1], 'o-', color='red')
ax2.set_xlim(-4.5, 4.5)
ax2.set_ylim(-4.5, 4.5)
ax2.set_xlabel("$X_0$")
ax2.set_ylabel("$X_1$")
ax2.set_title("2D Visualization")

# Display the figure
plt.tight_layout()
plt.show()
<Figure size 1500x800 with 2 Axes>

3.2.3ニューラルネットワークに対する勾配

ニューラルネットワークにおいて、重みパラメータの勾配を求める計算を確認します。

ここで、形状が2×32 \times 3の重みW\mathbf{W}を持つニューラルネットワークがあり、損失関数をLLで表すことを考えましょう。この場合、勾配はLW\frac{\partial L}{\partial \mathbf{W}}で表すことができます。

W=(w0,0w0,1w0,2w1,0w1,1w1,2),LW=(Lw0,0Lw0,1Lw0,2Lw1,0Lw1,1Lw0,2)\mathbf{W} = \begin{pmatrix} w_{0,0} & w_{0,1} & w_{0,2} \\ w_{1,0} & w_{1,1} & w_{1,2} \end{pmatrix}, \frac{\partial L}{\partial \mathbf{W}} = \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \frac{\partial L}{\partial w_{0,2}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{1,1}} & \frac{\partial L}{\partial w_{0,2}} \end{pmatrix}
# (仮の)入力データを作成
x = np.array([0.6, 0.9])
print(x)

# (仮の)教師データを作成
t = np.array([0, 0, 1])
print(t)
[0.6 0.9]
[0 0 1]
np.random.seed(0) # 乱数のシードを固定
class simplenet:
    def __init__(self):
        self.W = np.random.randn(2, 3)# 重みを初期化する関数を定義
    def predict(self, x):
        return np.dot(x, self.W) # 重み付き和を計算
    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z) # ソフトマックス関数による正規化
        loss = cross_entropy_error(y, t) # 交差エントロピー誤差を計算
        return loss 
net = simplenet()
p = net.predict(x)
print(p)
[ 3.07523529  1.92089652 -0.2923073 ]
net.loss(x, t)
3.6674507891066104

続いて、勾配を求めてみましょう。

# 損失メソッドを実行する関数を作成
def f(W):
    # 損失メソッドを実行
    return net.loss(x, t)
# 損失を計算
L = f(net.W)
print(L)
3.6674507891066104
# 重みの勾配を計算
dW = numerical_gradient(f, net.W)
print(dW)
[[ 0.44452826  0.14014461 -0.58467287]
 [ 0.66679239  0.21021692 -0.87700931]]

これで重みの勾配LW\frac{\partial L}{\partial \mathbf{W}}を得られました。

その中身を見ると、例えば、LW1,1\frac{\partial L}{\partial \mathbf{W_{1,1}}}はおよそ0.44ということは、w1,1w_{1,1}hhだけ増やすと損失関数の値は0.44h0.44hだけ増加することを意味します。

そのため、損失関数の値を減らすために、w1,1w_{1,1}はマイナス方向へ更新するのが良いことがわかりました。

パラメータの勾配が得られたということは、パラメータの学習を行えるようになったということです。

42層ニューラルネットワークの実装

これまでに勉強した、「損失関数」、「ミニバッチ」、「勾配」、「勾配下降法」をまとめて、ニューラルネットワークの学習手順を確認します。

  • ミニバッチ: データセットからミニバッチをランダムに取り出す。ここでは、そのミニバッチの損失関数の値を減らすことを目的とする。

  • 勾配の算出:各重みパラメータの勾配を求める。

  • パラメーターの更新:重みパラメータを勾配方向に微少量だけ更新する。

  • 収束するまで繰り返す

ここでは、2層のニューラルネットワークの計算と最適化プロセスを確認しましょう。

4.1数式の確認

4.1.1入力層

ニューラルネットワークの入力X\mathbf{X}、第1層の重みW(1)\mathbf{W^{(1)}}b(1)\mathbf{b}^{(1)}を次の形状とします。

X=(x0,0x0,1x0,D1x1,0x1,1x1,D1xN1,0xN1,1xN1,D1), W(1)=(w0,0w0,1w0,H1w1,0w1,1w1,H1wD1,0wD1,1wD1,H1), b(1)=(b0b1bH1)\mathbf{X} = \begin{pmatrix} x_{0,0} & x_{0,1} & \cdots & x_{0,D-1} \\ x_{1,0} & x_{1,1} & \cdots & x_{1,D-1} \\ \vdots & \vdots & \ddots & \cdots \\ x_{N-1,0} & x_{N-1,1} & \cdots & x_{N-1,D-1} \end{pmatrix} ,\ \mathbf{W}^{(1)} = \begin{pmatrix} w_{0,0} & w_{0,1} & \cdots & w_{0,H-1} \\ w_{1,0} & w_{1,1} & \cdots & w_{1,H-1} \\ \vdots &\vdots & \ddots & \vdots \\ w_{D-1,0} & w_{D-1,1} & \cdots & w_{D-1,H-1} \end{pmatrix} ,\ \mathbf{b}^{(1)} = \begin{pmatrix} b_0 & b_1 & \cdots & b_{H-1} \end{pmatrix}

ここで、N\mathbf{N}はバッチサイズ、D\mathbf{D}は各データxn=(xn,0,,xn,D1)\mathbf{x}_n = (x_{n,0}, \cdots, x_{n,D-1})の要素数、H\mathbf{H}は中間層のニューロン数です。

4.1.2隠れ層

1層の重み付き和A(1)\mathbf{A}^{(1)}を計算します。

A(1)=XW(1)+B(1)\mathbf{A}^{(1)} = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{B}^{(1)}

N×D\mathbf{N} \times \mathbf{D}D×H\mathbf{D} \times \mathbf{H}の行列の積なので、計算結果はN×H\mathbf{N} \times \mathbf{H}の行列になります。

A(1)=(a0,0a0,1a0,H1a1,0a1,1a1,H1aN1,0aN1,1aN1,H1)\mathbf{A}^{(1)} = \begin{pmatrix} a_{0,0} & a_{0,1} & \cdots & a_{0,H-1} \\ a_{1,0} & a_{1,1} & \cdots & a_{1,H-1} \\ \vdots & \vdots & \ddots & \cdots \\ a_{N-1,0} & a_{N-1,1} & \cdots & a_{N-1,H-1} \end{pmatrix}

重み付き和A(1)\mathbf{A}^{(1)}の各要素をシグモイド関数により活性化します。

zn,h=sigmoid(an,h)z_{n,h} = \mathrm{sigmoid}(a_{n,h})

ただ、活性化関数は形状(N×H\mathbf{N} \times \mathbf{H})に影響しません。第1層の出力の結果は、

Z=(z0,0z0,1z0,H1z1,0z1,1z1,H1zN1,0zN1,1zN1,H1)\mathbf{Z} = \begin{pmatrix} z_{0,0} & z_{0,1} & \cdots & z_{0,H-1} \\ z_{1,0} & z_{1,1} & \cdots & z_{1,H-1} \\ \vdots & \vdots & \ddots & \cdots \\ z_{N-1,0} & z_{N-1,1} & \cdots & z_{N-1,H-1} \end{pmatrix}

になります。

4.1.3出力層

次に、第2層の重みW(2)\mathbf{W^{(2)}}b(2)\mathbf{b}^{(2)}は以下のように形状しています。

W(2)=(w0,0w0,1w0,K1w1,0w1,1w1,K1wH1,0wH1,1wH1,K1), b(2)=(b0b1bK1)\mathbf{W}^{(2)} = \begin{pmatrix} w_{0,0} & w_{0,1} & \cdots & w_{0,K-1} \\ w_{1,0} & w_{1,1} & \cdots & w_{1,K-1} \\ \vdots &\vdots & \ddots & \vdots \\ w_{H-1,0} & w_{H-1,1} & \cdots & w_{H-1,K-1} \end{pmatrix} ,\ \mathbf{b}^{(2)} = \begin{pmatrix} b_0 & b_1 & \cdots & b_{K-1} \end{pmatrix}

ここで、H\mathbf{H}は中間層のニューロン数、K\mathbf{K}は出力層のクラス数です。

第2層の重み付き和A(2)\mathbf{A}^{(2)}

A(2)=ZW(2)+B(2)\mathbf{A}^{(2)} = \mathbf{Z} \mathbf{W}^{(2)} + \mathbf{B}^{(2)}

N×H\mathbf{N} \times \mathbf{H}H×K\mathbf{H} \times \mathbf{K}の行列の積なので、計算結果はN×K\mathbf{N} \times \mathbf{K}の行列になります。

A(2)=(a0,0a0,1a0,K1a1,0a1,1a1,K1aN1,0aN1,1aN1,K1)\mathbf{A}^{(2)} = \begin{pmatrix} a_{0,0} & a_{0,1} & \cdots & a_{0,K-1} \\ a_{1,0} & a_{1,1} & \cdots & a_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ a_{N-1,0} & a_{N-1,1} & \cdots & a_{N-1,K-1} \end{pmatrix}

ここで、ソフトマックス関数により各データの重み付き和an(2)a_{n}^{(2)}を活性化して、ニューラルネットワークの出力yny_nとします。

yn=softmax(an(2))\mathbf{y}_n = \mathrm{softmax}(\mathbf{a}_n^{(2)})

Y\mathbf{Y}でニューラルネットワークの出力を表します。nn番目のデータに関する出力yn\mathbf{y}_nは、0yn,k10 \leq y_{n,k} \leq 1k=0K1yn,k=1\sum_{k=0}^{K-1} y_{n,k} = 1に正規化されており、nn番目の入力データxn\mathbf{x}_nがどのクラスのかを表す確率分布として扱えるのでした。

Y=(y0,0y0,1y0,K1y1,0y1,1y1,K1yN1,0yN1,1yN1,K1)\mathbf{Y} = \begin{pmatrix} y_{0,0} & y_{0,1} & \cdots & y_{0,K-1} \\ y_{1,0} & y_{1,1} & \cdots & y_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ y_{N-1,0} & y_{N-1,1} & \cdots & y_{N-1,K-1} \end{pmatrix}

4.1.4損失の計算

NN個のデータに関する教師データT\mathbf{T}は、出力Y\mathbf{Y}と同じ形状になります。

T=(t0,0t0,1t0,K1t1,0t1,1t1,K1tN1,0tN1,1tN1,K1)\mathbf{T} = \begin{pmatrix} t_{0,0} & t_{0,1} & \cdots & t_{0,K-1} \\ t_{1,0} & t_{1,1} & \cdots & t_{1,K-1} \\ \vdots & \vdots & \ddots & \cdots \\ t_{N-1,0} & t_{N-1,1} & \cdots & t_{N-1,K-1} \end{pmatrix}

特に、分類問題の場合、各データの教師データtnt_nは、、正解ラベルが1でそれ以外が0といった形になります。

(平均)交差エントロピー誤差を計算して、損失LLとします。

L=1Nn=0N1k=0K1tn,klogyn,kL = - \frac{1}{N} \sum_{n=0}^{N-1} \sum_{k=0}^{K-1} t_{n,k} \log y_{n,k}

4.1.5勾配の計算

損失を求めるまでの計算を1つの関数とみなして、重みの勾配LW\frac{\partial L}{\partial \mathbf{W}}とバイアスの勾配Lb\frac{\partial L}{\partial \mathbf{b}}を求めます。

1層のパラメータW(1), b(1)\mathbf{W}^{(1)},\ \mathbf{b}^{(1)}LW(1), Lb(1)\frac{\partial L}{\partial \mathbf{W}^{(1)}},\ \frac{\partial L}{\partial \mathbf{b}^{(1)}}で表します。

LW(1)=(Lw0,0Lw0,1Lw0,H1Lw1,0Lwa,1Lw1,H1LwD1,0LwD1,1LwD1,H1), Lb(1)=(Lb0Lb1LbH1)\frac{\partial L}{\partial \mathbf{W}^{(1)}} = \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \cdots & \frac{\partial L}{\partial w_{0,H-1}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{a,1}} & \cdots & \frac{\partial L}{\partial w_{1,H-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial w_{D-1,0}} & \frac{\partial L}{\partial w_{D-1,1}} & \cdots & \frac{\partial L}{\partial w_{D-1,H-1}} \\ \end{pmatrix} ,\ \frac{\partial L}{\partial \mathbf{b}^{(1)}} = \begin{pmatrix} \frac{\partial L}{\partial b_0} & \frac{\partial L}{\partial b_1} & \cdots & \frac{\partial L}{\partial b_{H-1}} \end{pmatrix}

同様に、第2層のパラメータW(2), b(2)\mathbf{W}^{(2)},\ \mathbf{b}^{(2)}LW(2), Lb(2)\frac{\partial L}{\partial \mathbf{W}^{(2)}},\ \frac{\partial L}{\partial \mathbf{b}^{(2)}}で表します。

LW(2)=(Lw0,0Lw0,1Lw0,K1Lw1,0Lwa,1Lw1,K1LwH1,0LwH1,1LwH1,K1), Lb(2)=(Lb0Lb1LbK1)\frac{\partial L}{\partial \mathbf{W}^{(2)}} = \begin{pmatrix} \frac{\partial L}{\partial w_{0,0}} & \frac{\partial L}{\partial w_{0,1}} & \cdots & \frac{\partial L}{\partial w_{0,K-1}} \\ \frac{\partial L}{\partial w_{1,0}} & \frac{\partial L}{\partial w_{a,1}} & \cdots & \frac{\partial L}{\partial w_{1,K-1}} \\ \vdots & \vdots & \ddots & \vdots \\ \frac{\partial L}{\partial w_{H-1,0}} & \frac{\partial L}{\partial w_{H-1,1}} & \cdots & \frac{\partial L}{\partial w_{H-1,K-1}} \\ \end{pmatrix} ,\ \frac{\partial L}{\partial \mathbf{b}^{(2)}} = \begin{pmatrix} \frac{\partial L}{\partial b_0} & \frac{\partial L}{\partial b_1} & \cdots & \frac{\partial L}{\partial b_{K-1}} \end{pmatrix}

各要素は、それぞれパラメータの対応する要素の偏微分です。

4.1.6パラメータの更新

各パラメータの勾配、LW, Lb\frac{\partial L}{\partial \mathbf{W}},\ \frac{\partial L}{\partial \mathbf{b}}を用いて、勾配降下法によりパラメータW, b\mathbf{W},\ \mathbf{b}を更新します。

更新後のパラメータをW(new), b(new)\mathbf{W}^{(\mathrm{new})},\ \mathbf{b}^{(\mathrm{new})}とすると、更新式は次の式で表せます。

W(new)=WηLWb(new)=bηLb\begin{aligned} \mathbf{W}^{(\mathrm{new})} &= \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}} \\ \mathbf{b}^{(\mathrm{new})} &= \mathbf{b} - \eta \frac{\partial L}{\partial \mathbf{b}} \end{aligned}

各要素に注目すると、それぞれ次の計算をしています。

wh,k(new)=wh,kηLwh,kbk(new)=bkηLbk\begin{aligned} w_{h,k}^{(\mathrm{new})} &= w_{h,k} - \eta \frac{\partial L}{\partial w_{h,k}} \\ b_k^{(\mathrm{new})} &= b_k - \eta \frac{\partial L}{\partial b_k} \end{aligned}