からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

4.2:損失関数【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」学習初手『ゼロから作るDeep Learning』民のための実装攻略ノートです。『ゼロつく1』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ処理を確認しながらゆっくりと組んでいきます。

 この記事は、4.2節「損失関数」の内容になります。損失関数として用いる2乗和誤差と交差エントロピー誤差をPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

4.2 損失関数

 ニューラルネットワークの学習において、性能の悪さを示す指標として損失関数を導入します。損失関数として、2乗和誤差や交差エントロピー誤差などが用いられます。この節では、この2つの誤差を実装します。

4.2.1 2乗和誤差

 2乗和誤差は次の式です。

$$ E = \frac{1}{2} \sum_k ( y_k - t_k )^2 \tag{4.1} $$

 (最終層にソフトマックス関数を用いた)ニューラルネットワークの出力と(one-hot表現の)教師データの各要素の差に注目します。各要素の差の2乗和を2で割った値が2乗和誤差になります。この定義式自体の解釈については【損失関数【ゼロつく1のノート(数学)】 - からっぽのしょこ】で確認します。

 ソフトマックス関数の出力は、確率として解釈できるのでした。そして教師データは、正解ラベルが1、それ以外は0となっています。これは、正解の要素を100%それ以外を0%の確率として解釈できます。つまりこの差を確率値の予測誤差として扱います。

 では実装しましょう。まずは(このためにニューラルネットワークから出力するのも手間なので手打ちで)出力データyと教師データtを用意します。

# NumPyを読み込む
import numpy as np

# 仮の出力データを設定
y = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])

# 仮の教師データを設定
t = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

 このデータを使って、式(4.1)を1つずつ再現していきます。

 まずは各要素の差をとります。つまり$y_k - t_k$の計算をします。

# 各要素の差を計算
tmp = y - t
print(tmp)
[ 0.1   0.05 -0.4   0.    0.05  0.1   0.    0.1   0.    0.  ]

 各要素が-1から1までの値になります。

 次に各要素を2乗します。$(y_k - t_k)^2$に対応します。

# 各要素の差の2乗を計算
tmp = (y - t) ** 2
print(tmp)
[0.01   0.0025 0.16   0.     0.0025 0.01   0.     0.01   0.     0.    ]

 2乗したことで、全ての要素が正の値になりました。

 各要素の計算結果を全て足します。$\sum_{k=1}^{10} (y_k - t_k)^2$です。

# 各要素の差の2乗の総和を計算
tmp = np.sum((y - t) ** 2)
print(tmp)
0.19500000000000006

 要素数10の1次元配列がスカラーになりました。

 最後に2で割ります(0.5を掛けます)。

# 2乗和誤差を計算:式(4.1)
E = 0.5 * np.sum((y - t) ** 2)
print(E)
0.09750000000000003

 以上が式(4.1)の計算になります。

 計算の過程に問題がなければ、関数として実装しましょう。(ちなみにmean squared errorという訳は誤植です。お使いの本がsum of squared errorとなっていなければ正誤表を確認しましょう。正誤表は「はじめに」にあるURLのページの「README.md」か「Wiki」タブからアクセスできます。)

# 2乗和誤差を定義
def sum_squared_error(y, t):
    return 0.5 * np.sum((y - t) ** 2) # 式(4.1)


 同じデータを使って作成した関数でも計算してみましょう。

# 2乗和誤差を計算
E = sum_squared_error(y, t)
print(E)
0.09750000000000003


 損失関数として用いられる2乗和誤差を実装できました。次は交差エントロピー誤差を実装します。

4.2.2 交差エントロピー誤差

 交差エントロピー誤差は次の式です。

$$ E = - \sum_k t_k \log y_k \tag{4.2} $$

 (最終層にソフトマックス関数を用いた)ニューラルネットワークの出力と(one-hot表現の)教師データの正解ラベルの要素にのみ注目します。正解ラベル以外の値は0になるので、正解ラベルに対応する出力$y_k$の自然対数の符号を反転させた値が交差エントロピー誤差になります。この定義式自体の解釈については【損失関数【ゼロつく1のノート(数学)】】で確認します。

 正解ラベルの確率$y_k$が0に近づくほど$- \log y_k$が0に近づきます。これを予測の誤差と解釈したものです。

 では実装しましょう。まずは(このためにニューラルネットワークから出力するのも手間なので手打ちで)出力データyと教師データtを用意します。

# 仮の出力データを設定
y = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])

# 仮の教師データを設定
t = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

 このデータを使って交差エントロピー誤差を計算していきますが、ここで1つ問題があります。

 $\log 0$は計算できません($a^x = 0$を満たす$x$は存在しません)。

# log 0の計算
np.log(0)
-inf

 エラーとはならず、$-\infty$が返ってきます。

 この対策として、誤差レベルの小さな値を加えて対数をとります。ここでは1e-7を加えることにします。(このeはネイピア数ではなく10のことで、)1e-7は$1e-7 = 1 * 10^{-7} = \frac{1}{10^7} = 0.0000001$を意味します。

# 微小な値を設定
delta = 1e-7

# log 0対策
np.log(0 + delta)
-16.11809565095832


 出力yの各要素にdeltaを加えて対数をとります。$\log y_k$を求めます。

# 出力の対数をとる
tmp = np.log(y + delta)
print(tmp)
[ -2.30258409  -2.99573027  -0.51082546 -16.11809565  -2.99573027
  -2.30258409 -16.11809565  -2.30258409 -16.11809565 -16.11809565]

 yの要素は0から1の値のため、対数をとると0以下の値になります。

 対数をとったyと教師データtを掛けます。$t_k \log y_k$の計算をします。

# 教師データと出力の対数の積を計算
tmp = t * np.log(y + delta)
print(tmp)
[-0.         -0.         -0.51082546 -0.         -0.         -0.
 -0.         -0.         -0.         -0.        ]

 正解ラベルの要素の値はそのままで、それ以外の要素は0になります。

 全ての要素を足します。$\sum_{k=1}^{10} t_k \log y_k$に対応します。

# 教師データと出力の対数の積の総和を計算
tmp = np.sum(t * np.log(y + delta))
print(tmp)
-0.510825457099338

 総和をとりますが、正解ラベルに関する誤差にのみ注目していることが分かりますね。またこの処理でスカラーになります。

 ここまでの計算結果は常に負の値になるので、最後に符号を反転させることで交差エントロピー誤差は常に正の値をとるようになります。

# 交差エントロピー誤差を計算
E = -np.sum(t * np.log(y + delta))
print(E)
0.510825457099338

 以上が式(4.2)の計算になります。

 計算過程に問題がなければ、関数として実装します。

# 交差エントロピー誤差を定義
def cross_entropy_error(y, t):
    delta = 1e-7 # log 0とならないように微小な値
    return - np.sum(t * np.log(y + delta)) # 式(4.2)


 関数を作成できたら、同じデータを使って計算してみましょう。

# 交差エントロピー誤差を計算
E = cross_entropy_error(y, t)
print(E)
0.510825457099338

 以上が交差エントロピー誤差の実装になります。

 ここでは1つの画像データに対する誤差の計算を行いましたが、次は複数のデータを1度に処理する場合を実装します。

4.2.3 ミニバッチ学習

 訓練データが複数の場合の交差エントロピー誤差の計算式は次のようになります。

$$ E = - \frac{1}{N} \sum_{n} \sum_{k} t_{nk} \log y_{nk} \tag{4.3} $$

 データ数が1の場合(4.2)との違いなどは【損失関数【ゼロつく1のノート(数学)】】で確認してください。また式(4.3)の実装は次項で行います。

 この項では、訓練データから指定したバッチサイズのデータをランダムに取り出します。

 まずは画像データを読み込みます。MNISTデータセットの読み込みについては【MNISTデータセットの読み込み【ゼロつく1のノート(Python)】】で確認してください。

# データ読み込み用ライブラリを読み込む
import sys
import os

# 読み込みたいファイルのあるディレクトリを指定
sys.path.append('C:\\Users\\「ユーザー名」\\Documents\\・・・\\deep-learning-from-scratch-master')

# ファイルにアクセスして関数を読み込む
from dataset.mnist import load_mnist

# 画像データを取得
(x_train, t_train), (x_test, t_test) = load_mnist(normalize = True, one_hot_label = True)
print(x_train.shape) # 訓練画像
print(t_train.shape) # 訓練ラベル
(60000, 784)
(60000, 10)

 学習に用いる画像データは6万枚分あり、訓練画像x_trainと訓練ラベルt_trainのセットです。訓練画像は784のピクセルデータを、訓練ラベルは0から9までのラベルデータをそれぞれ列の要素として持ちます。

 ここからバッチサイズ分のデータをランダムに抽出します。

 そのために必要な、np.random.choice()によるランダムな数値リストの生成を確認します。

# NumPyを読み込む
import numpy as np

# 値をランダムに生成
np.random.choice(100, 10, replace=False)
array([37, 89, 49, 73, 71, 28, 25, 18,  1,  0])

 0から第1引数に指定した値未満の自然数から、第2引数に指定した個数の値をランダムに生成します。
 replace引数にFalseを指定すると、重複することなく値を生成します。Trueとすると重複を許します。デフォルトはTrueなので、replace引数を指定しなければ、値が重複することがあります。

# 値をランダムに生成
print(np.random.choice(10, 10)) # 重複を許す
print(np.random.choice(10, 10, replace=False)) # 重複しない
[6 4 7 7 4 8 2 9 4 8]
[2 5 9 3 7 1 4 8 6 0]


 np.random.choice()の第1引数に指定するために、訓練画像x_trainの行数(データ数)を調べてtrain_sizeとします。

# データ数(画像枚数)
train_size = x_train.shape[0]
print(train_size)
60000


 batch_sizeに1回の処理に用いるデータ数を指定して、np.random.choice()でランダムに取り出すデータ番号を生成します。

# バッチサイズ
batch_size = 10

# 抽出データのインデックス
batch_mask = np.random.choice(train_size, batch_size, replace=False)
print(batch_mask)
[27670 32639 49065  7245 47683 33366 54630  3303 53033 40819]

 0からtrain_size未満(最初の要素が0なので最後の要素はデータ数60000-1番目)からbatch_size個の数値を重複せずに生成します。

 このbatch_maskを添字として指定することで、対応する訓練画像と訓練ラベルを抽出します。

# データを抽出
x_batch = x_train[batch_mask] # 訓練画像
t_batch = t_train[batch_mask] # 訓練ラベル
print(x_batch)
print(t_batch)
[[0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 ...
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]
 [0. 0. 0. ... 0. 0. 0.]]
[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]

 これで必要な分の訓練データを取り出すことができました。(x_batchの要素が0ばかりなのは最初と最後のピクセルデータのみを表示しているからです(端の方には線がこないので真っ黒を示す0となることが多いです)。 )

 次項では、このデータを入力としたときのニューラルネットワークの出力を評価する(ことを想定して)交差エントロピー誤差を実装します。

4.2.4 「バッチ対応版」交差エントロピー誤差の実装

 続いて、複数の訓練データに対する交差エントロピー誤差を計算します。計算式は次のようになります。

$$ E = - \frac{1}{N} \sum_n \sum_k t_{nk} \log y_{nk} \tag{4.3} $$

 各データごとの交差エントロピー誤差$\sum_k t_{nk} \log y_{nk}$を、1枚目から$N$枚目までの全データ分足して、データ数$N$で割った値が複数データ版の交差エントロピー誤差になります。

 実装していくにあたっての処理の確認のため、ソフトマックス関数の出力と教師データを用意します。

# NumPyを読み込む
import numpy as np

# 仮の出力データを設定
y = np.array([
    [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0], 
    [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], 
    [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
])
print(y)
print(y.shape) # 各次元の要素数
print(y.ndim) # 次元数
[[0.1  0.05 0.6  0.   0.05 0.1  0.   0.1  0.   0.  ]
 [0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1  0.1 ]
 [0.1  0.05 0.1  0.   0.05 0.1  0.   0.6  0.   0.  ]]
(3, 10)
2
# 仮の教師データ
t = np.array([
    [0, 0, 1, 0, 0, 0, 0, 0, 0, 0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 
    [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
])
print(t)
print(t.shape) # 各次元の要素数
print(t.ndim) # 次元数
[[0 0 1 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 1 0]
 [0 0 1 0 0 0 0 0 0 0]]
(3, 10)
2

 このデータを使って、式(4.3)の計算を1つずつ確認していきます。

 先ほどと同様に、全ての要素に小さな値を加えてから対数をとります。$\log y_{nk}$ですね。

# 微小な値を用意
delta = 1e-7

# 出力の対数をとる
tmp = np.log(y + delta)
print(np.round(tmp, 2))
[[ -2.3   -3.    -0.51 -16.12  -3.    -2.3  -16.12  -2.3  -16.12 -16.12]
 [ -2.3   -2.3   -2.3   -2.3   -2.3   -2.3   -2.3   -2.3   -2.3   -2.3 ]
 [ -2.3   -3.    -2.3  -16.12  -3.    -2.3  -16.12  -0.51 -16.12 -16.12]]

 全ての要素が0以下の値になるのでした。

 対数をとったyと教師データtを要素ごとに掛けます。$t_{nk} \log y_{nk}$の計算を行います。

# 教師データと出力の対数の積を計算
tmp = t * np.log(y + delta)
print(np.round(tmp, 2))
[[-0.   -0.   -0.51 -0.   -0.   -0.   -0.   -0.   -0.   -0.  ]
 [-0.   -0.   -0.   -0.   -0.   -0.   -0.   -0.   -2.3  -0.  ]
 [-0.   -0.   -2.3  -0.   -0.   -0.   -0.   -0.   -0.   -0.  ]]

 正解ラベル以外の要素は全て0になります。

 この結果を全て足します。$\sum_{n=1}^{10} \sum_{k=1}^{10} t_{nk} \log y_{nk}$の計算に対応します。

# 教師データと出力の対数の積の和を計算
tmp = np.sum(t * np.log(y + delta))
print(tmp)
-5.11599364308843

 全ての要素の和をとるので、2次元配列からスカラーになります。また、この値は常に0以下の値になるのでした。

 最後にバッチサイズ(データ数)$N$で割る必要があります。なので、データ数を調べましょう。変数名.shapeで各次元の要素数を返します。そこから[0]で、0番目の要素を取り出します。
 この例では2次元配列のデータを扱っているので、0番目の値は行数のことです。

# データ数(バッチサイズ)
batch_size = y.shape[0]
print(batch_size)
3


 符号を反転させ、バッチサイズで割った値が交差エントロピー誤差となります。

# 交差エントロピー誤差を計算:式(4.2)
E = - np.sum(t * np.log(y + delta)) / batch_size
print(E)
1.70533121436281

 以上が式(4.3)の計算過程になります。

 作成する関数に渡されるデータが2次元配列のデータのみの場合はこの処理でいいのですが、1次元配列(データ数が1)の場合もあるならそれ用の処理が必要になります。(本当にムリか?)

 次元数を変更するメソッド変数名.reshape()を使って、1次元配列のデータを2次元配列に変換します。

# バッチサイズ1のニューラルネットワークの出力データ
vec = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
print(vec)
print(vec.shape) # 各次元の要素数
print(vec.ndim) # 次元数
print(vec.size) # 要素数
[0.1  0.05 0.6  0.   0.05 0.1  0.   0.1  0.   0.  ]
(10,)
1
10

 yは、要素数が10の1次元配列であることが分かります。

 これを$1 \times 10$の行列に変換します。

# データの形状を変更
mat = vec.reshape(1, vec.size)
print(mat)
print(mat.shape) # 各次元の要素数
print(mat.ndim) # 次元数
print(mat.size) # 要素数
[[0.1  0.05 0.6  0.   0.05 0.1  0.   0.1  0.   0.  ]]
(1, 10)
2
10

 2次元配列に変換できていることが確認できました。

 条件分岐ifを使って、次元数が1(.ndimが1)のときのみこの処理を行うように実装します。

# ミニバッチ対応版交差エントロピー誤差:one-hot表現版
def cross_entropy_error(y, t):
    
    # 1次元配列の場合
    if y.ndim == 1:
        # 1×Nの行列に変換
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    
    # データ数(N)
    batch_size = y.shape[0] # 行数
    
    # 交差エントロピー誤差の計算
    return - np.sum(t * np.log(y + 1e-7)) / batch_size # 式(4.3)


 作成した関数でも同じデータを使って計算してみましょう。

# 交差エントロピー誤差を計算
E = cross_entropy_error(y, t)
print(E)
1.70533121436281

 以上で、複数データに対する交差エントロピー誤差を実装できました。

 続いて、教師データがone-hot表現ではなく正解ラベルを数値で示す場合の交差エントロピー誤差を実装します。ただしこちらのパターンは利用しない(なかったはず)ので、両方やって混乱するくらいなら飛ばしてもいいと思います。

 処理の違いは必要な要素の取り出し方です。one-hot表現は不要な要素に0を掛ける(て全て足す)ことで、必要な要素の値のみを残しました。こちらの場合は、tを添字として使うことで、必要な要素を取り出します。

 まずは2次元配列から指定した要素を取り出す方法を確認しましょう。

A = np.array([
    [0, 1, 2], 
    [3, 4, 5], 
    [6, 7, 8]
])

# 要素を抽出
A[0, 0]
0

 このように、添字に行番号と列番号を指定することで行列の要素を取り出せました。

 行番号と列番号を複数指定することで、複数のデータを取り出すことができます。

B = np.array([
    [0, 1, 0, 0, 0], 
    [0, 0, 2, 0, 0], 
    [0, 0, 0, 0, 4], 
    [9, 0, 0, 0, 0], 
    [0, 0, 0, 3, 0]
])

# 要素を抽出
B[np.array([0, 1, 2, 3, 4]), np.array([1, 2, 4, 0, 3])]
array([1, 2, 4, 9, 3])

 この機能を利用します。

 行番号には、np.arange(batch_size)で生成する0からバッチサイズまでの自然数を指定します。列番号は訓練ラベルをそのまま利用します。

# 仮の教師データを設定
t = np.array([2, 8, 2])

# バッチサイズ(データ数)
batch_size = y.shape[0]

# 教師ラベルの要素を抽出
y[np.arange(batch_size), t]
array([0.6, 0.1, 0.1])

 教師データtを添字として利用することで、必要な要素のみを抽出できました。

 この抽出した要素をnp.log()で処理することが$\sum_{k=1}^{10} t_{nk} \log y_{nk}$の計算に対応します。

 あとは上の例と同様の処理で実装できます。

# 交差エントロピー誤差:1要素版
def cross_entropy_error_2(y, t):
    
    # 1次元配列の場合
    if y.ndim == 1:
        # 1×Nの行列に変換
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    
    # データ数(N)
    batch_size = y.shape[0]
    
    # 交差エントロピー誤差の計算
    return - np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size # 式(4.3)


 こちらも同じデータで計算してみます。

# 交差エントロピー誤差を計算
E = cross_entropy_error(y, t)
print(E)
1.70533121436281


 以上で交差エントロピー誤差の実装ができました!この値を小さくすることで学習を進めます。次節からはこの誤差が小さくなるようにパラメータを調整する方法を説明します。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.

おわりに

 現在の重み$\mathrm{W}$があり、重みによって出力$\mathrm{Y}$が決まります。その出力$\mathrm{Y}$と教師データ$\mathrm{T}$から、損失関数によって誤差$E$を求めます。その損失関数の勾配を基にして誤差$E$が小さくなるように(=勾配降下法によって)、重み$\mathrm{W}$の値を少しだけ調整します。
 値を更新した重み$\mathrm{W}$によって出力$\mathrm{Y}$が決まります。・・・

 このように、重み$\mathrm{W}$の値を少しずつ繰り返し更新することを「学習」と呼びます。深層「学習」と言うくらいですから、この一連の流れが大きな目的の1つです。
 4章では、微分やなんやと色々登場しますが(また入力$\mathbf{X}$と混同するかもしれませんが)、全ては重み$\mathrm{W}$と(省略しましたが)バイアス$\mathbf{B}$の学習のための道具だと思えば対応関係を整理できるかと思います。ちなみに入力$\mathbf{X}$は画像データのことなので既に決まった値です(変数ではありません)。

 なんかURLの埋め込みがうまくいかないことがある、、、

【次節の内容】

www.anarchive-beta.com