2.2 SoftmaxCrossEntropy

2.2.1 定式化

ソフトマックス関数と交差エントロピー誤差を合わせたSoftmaxCrossEntropyレイヤを実装します。ソフトマックス関数は、

y_k = \frac{\exp(x_k)}{\sum_i\exp(x_i)}

交差エントロピー誤差は、

L = -\sum_k \delta_{tk}\log y_k

です。ここでkは多クラス分類するときのクラス番号に相当します。

まずは、形状を定義します。

N, T, M = 2, 3, 4

[2] 2019-06-12 20:00:51 (5.01ms) python3 (183ms)

ここでNはバッチ数、Tはタイムステップ数、Mは出力の次元、すなわち、分類するクラス数です。形状の確認を行います。ソフトマックス関数の入力(スコア)を\mathbf{X}、出力(確率)を\mathbf{Y}、また、交差エントロピー誤差のターゲットを3、出力をLとします。

時系列データでない場合

パラメータ 形状 具体例
\mathbf{X} (N, M) (2, 4)
\mathbf{Y} (N, M) (2, 4)
\mathbf{T} (N,) (2,)
L () ()

時系列データの場合

パラメータ 形状 具体例
\mathbf{X} (N, T, M) (2, 3, 4)
\mathbf{Y} (N, T, M) (2, 3, 4)
\mathbf{T} (N, T) (2, 3)
L () ()

2.2.2 時系列データでない場合

入力を乱数で発生させます。

import numpy as np

x = np.random.randn(N, M)
x

[23] 2019-06-12 20:00:52 (31.2ms) python3 (415ms)

array([[ 1.11051294, -0.30251803,  0.70953014,  1.51345597],
       [ 0.159133  , -0.98412862, -2.08995385, -1.65864586]])

ソフトマックス関数では、オーバーフロー対策のため、バッチデータ(軸1)ごとに最大値を引きます。2次元配列を維持するように、keepdims=Trueとします。

x.max(axis=1, keepdims=True)

[24] 2019-06-12 20:00:52 (9.97ms) python3 (425ms)

array([[1.51345597],
       [0.159133  ]])

最大値を引いたものに指数関数を適用します。結果は0から1の範囲に収まります。

exp_x = np.exp(x - x.max(axis=1, keepdims=True))
exp_x

[25] 2019-06-12 20:00:52 (12.0ms) python3 (437ms)

array([[0.66835018, 0.16267938, 0.44756844, 1.        ],
       [1.        , 0.31877759, 0.10549551, 0.16238603]])

次にバッチデータ(軸1)ごとの和で正規化します。これが、ソフトマックス関数の出力になります。

y = exp_x / exp_x.sum(axis=1, keepdims=True)
y

[26] 2019-06-12 20:00:52 (7.06ms) python3 (444ms)

array([[0.29331641, 0.07139451, 0.19642273, 0.43886636],
       [0.63025509, 0.2009112 , 0.06648908, 0.10234462]])

当然、次は値が1の配列になります。

y.sum(axis=1)

[27] 2019-06-12 20:00:52 (15.6ms) python3 (460ms)

array([1., 1.])

次に、交差エントロピー誤差を求めます。ターゲットは、バッチ数分だけ正解ラベルが並んだベクトルです。

t = np.random.randint(0, M, N)
t

[28] 2019-06-12 20:00:52 (31.3ms) python3 (491ms)

array([1, 1])

例えば、上の例は、バッチデータ0の正解ラベルが1で、バッチデータ1の正解ラベルが1であることを示します。

交差エントロピー誤差はターゲットの位置にあるデータを取り出すことに相当するので、以下のように実装できます。

y_ = y[np.arange(N), t]
y_

[31] 2019-06-12 20:00:53 (7.02ms) python3 (513ms)

array([0.07139451, 0.2009112 ])

あとは対数の和を取りますが、無限小に発散することを防ぐために微小な値を付加します。また、バッチ数によらない結果を得るために、バッチ数で除算します。

-np.sum(np.log(y_ + 1e-7)) / N

[32] 2019-06-12 20:00:53 (15.6ms) python3 (529ms)

2.1222123454102375

「ゼロから作るDeep Learning」の実装と比較します。

from ivory.utils.repository import import_module

layers = import_module("scratch2/common.layers")
s = layers.SoftmaxWithLoss()
s.forward(x, t)

[33] 2019-06-12 20:00:53 (31.3ms) python3 (560ms)

2.1222123454102375

以上が、順伝搬になり、上のスカラー値が損失関数の値になります。

逆伝搬は、ソフトマックス関数と交差エントロピー誤差を合わせたレイヤの勾配が次式で与えられることを天下り的に認めたうえで、数値微分によって正しいことを確認します。

\partial L/x_k = y_k - \delta_{tk}
dx = y.copy()
dx[np.arange(N), t] -= 1
dx / N

[34] 2019-06-12 20:00:53 (8.05ms) python3 (568ms)

array([[ 0.1466582 , -0.46430275,  0.09821136,  0.21943318],
       [ 0.31512755, -0.3995444 ,  0.03324454,  0.05117231]])

「ゼロから作るDeep Learning」の実装と比較します。

s.backward()

[35] 2019-06-12 20:00:53 (7.05ms) python3 (575ms)

array([[ 0.1466582 , -0.46430275,  0.09821136,  0.21943318],
       [ 0.31512755, -0.3995444 ,  0.03324454,  0.05117231]])

SoftmaxCrossEntropyクラスの実装を確認しておきます。

Code 2.3 SoftmaxCrossEntropyクラスの定義

class SoftmaxCrossEntropy(LossLayer):
    def forward(self):
        y = np.exp(self.x.d - self.x.d.max(axis=-1, keepdims=True))
        y /= y.sum(axis=-1, keepdims=True)
        self.y.d = y
        self.y_2d = self.y.d.reshape(-1, self.y.d.shape[-1])
        self.t_1d = self.t.d.reshape(-1)
        self.size = self.y_2d.shape[0]
        loss = self.y_2d[np.arange(self.size), self.t_1d]
        self.loss.d = -np.sum(np.log(loss + 1e-7)) / self.size

    def backward(self):
        self.y_2d[np.arange(self.size), self.t_1d] -= 1
        self.x.g = self.y_2d.reshape(*self.x.d.shape) / self.size

    def predict(self) -> float:
        return np.argmax(self.x.d, axis=1)

    @property
    def accuracy(self) -> float:
        return float(np.average(self.predict() == self.t.d))

実際にインスタンスを作成します。

from ivory.core.model import sequential

net = [("input", M), "softmax_cross_entropy"]
model = sequential(net)
layer = model.layers[0]
layer.parameters

[38] 2019-06-12 20:00:53 (31.2ms) python3 (656ms)

[<Input('SoftmaxCrossEntropy.1.x', (4,)) at 0x188401e7550>,
 <Output('SoftmaxCrossEntropy.1.y', (4,)) at 0x188400ce198>,
 <Input('SoftmaxCrossEntropy.1.t', ()) at 0x188401e73c8>,
 <Loss('SoftmaxCrossEntropy.1.loss', ()) at 0x188530925f8>]

入力とターゲットを代入し、順伝搬を行ってみます。

model.set_data(x, t)
model.forward()
print(model.loss)

[39] 2019-06-12 20:00:53 (12.0ms) python3 (668ms)

2.1222123454102375

逆伝搬を求めてみます。

model.backward()
print(layer.x.g)

[40] 2019-06-12 20:00:53 (7.04ms) python3 (675ms)

[[ 0.1466582  -0.46430275  0.09821136  0.21943318]
 [ 0.31512755 -0.3995444   0.03324454  0.05117231]]

数値微分による勾配と比較してみます。

print(model.numerical_gradient(layer.x.variable))

[41] 2019-06-12 20:00:53 (15.7ms) python3 (691ms)

[[ 0.146658   -0.4643021   0.09821123  0.21943287]
 [ 0.31512739 -0.3995442   0.03324453  0.05117229]]

正しいことが確認できました。

2.2.3 時系列の場合

入力を乱数で発生させます。

x = np.random.randn(N, T, M)
t = np.random.randint(0, M, (N, T))
model.set_data(x, t)
model.forward()
print(model.loss)

[42] 2019-06-12 20:00:53 (15.6ms) python3 (707ms)

1.5939269529048279

逆伝搬を求めてみます。

model.backward()
print(layer.x.g)

[43] 2019-06-12 20:00:53 (6.03ms) python3 (713ms)

[[[-0.12800894  0.08071058  0.02708951  0.02020885]
  [ 0.01406084  0.09369186 -0.12850196  0.02074925]
  [ 0.03716839  0.02091162  0.09977668 -0.15785669]]

 [[-0.08296067  0.03812998  0.02263854  0.02219215]
  [ 0.04141342  0.06148008 -0.15356399  0.05067049]
  [ 0.01152623  0.0081133   0.04141871 -0.06105824]]]

数値微分による勾配と比較してみます。

print(model.numerical_gradient(layer.x.variable))

[44] 2019-06-12 20:00:53 (11.0ms) python3 (724ms)

[[[-0.12800888  0.08071054  0.0270895   0.02020884]
  [ 0.01406084  0.09369182 -0.1285019   0.02074924]
  [ 0.03716832  0.02091158  0.09977649 -0.15785639]]

 [[-0.08296065  0.03812997  0.02263854  0.02219214]
  [ 0.04141337  0.06148    -0.15356379  0.05067043]
  [ 0.01152622  0.0081133   0.04141871 -0.06105823]]]

正しいことが確認できました。

「ゼロから作るDeep Learning」の実装と比較します。

from ivory.utils.repository import import_module

layers = import_module("scratch2/common.time_layers")
s = layers.TimeSoftmaxWithLoss()
print(s.forward(x, t))
print(s.backward())

[45] 2019-06-12 20:00:53 (9.84ms) python3 (733ms)

1.59392768433122
[[[-0.12800894  0.08071058  0.02708951  0.02020885]
  [ 0.01406084  0.09369186 -0.12850196  0.02074925]
  [ 0.03716839  0.02091162  0.09977668 -0.15785669]]

 [[-0.08296067  0.03812998  0.02263854  0.02219215]
  [ 0.04141342  0.06148008 -0.15356399  0.05067049]
  [ 0.01152623  0.0081133   0.04141871 -0.06105824]]]