2.1 SigmoidCrossEntropy

2.1.1 定式化

シグモイド関数と交差エントロピー誤差を合わせたSigmoidCrossEntropyレイヤを実装します。シグモイド関数は、

y = \frac1{1+\exp(-x)}

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

L = -\delta_{t0}\log (1-y) - \delta_{t1}\log y

です。ここで、\deltaはクローネッカーのデルタで、i=jのとき\delta_{ij}=1i\ne jのとき\delta_{ij}=0となります。

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

N, T = 3, 4

[2] 2019-06-12 20:00:46 (6.02ms) python3 (187ms)

ここでNはバッチ数、Tはタイムステップ数です。形状の確認を行います。シグモイド関数の入力(スコア)を\mathbf{X}、出力(確率)を\mathbf{Y}、また、交差エントロピー誤差のターゲットを4、出力をLとします。

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

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

時系列データの場合、

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

となります。

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

まず、時系列データでない場合を考え、入力を乱数で発生させます。

import numpy as np

x = np.random.randn(N)
x

[19] 2019-06-12 20:00:47 (8.08ms) python3 (417ms)

array([ 0.18325944, -0.60097398, -1.82407824])

シグモイド関数の出力を求めます。

y = 1 / (1 + np.exp(-x))
y

[20] 2019-06-12 20:00:47 (8.00ms) python3 (425ms)

array([0.54568707, 0.35412089, 0.13894524])

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

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

[21] 2019-06-12 20:00:47 (31.3ms) python3 (456ms)

array([1, 0, 1])

例えば、上の例は、バッチデータ0の正解ラベルが1で、バッチデータ1の正解ラベルが0であることを示します。交差エントロピー誤差の式に忠実に書くと、

[-np.log(yi) if ti else -np.log(1 - yi) for yi, ti in zip(y, t)]

[24] 2019-06-12 20:00:47 (8.97ms) python3 (487ms)

[0.6057096037062325, 0.4371429351987146, 1.9736754108588104]

となります。forループを使わないで書くと、

-np.log(np.c_[1 - y, y][np.arange(N), t])

[25] 2019-06-12 20:00:47 (8.02ms) python3 (495ms)

array([0.6057096 , 0.43714294, 1.97367541])

となります。発散を防ぐための微小値の加算とバッチ数分の平均化をすれば、交差エントロピー誤差が、以下のように求まります。

-np.sum(np.log(np.c_[1 - y, y][np.arange(N), t] + 1e-7)) / N

[26] 2019-06-12 20:00:47 (31.2ms) python3 (526ms)

1.0055089639910264

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

from ivory.utils.repository import import_module

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

[27] 2019-06-12 20:00:47 (16.8ms) python3 (543ms)

1.0055089639910264

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

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

\partial L/x = y - t
dx = (y - t) / N
dx

[28] 2019-06-12 20:00:47 (14.0ms) python3 (557ms)

array([-0.15143764,  0.1180403 , -0.28701825])

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

s.backward()

[29] 2019-06-12 20:00:47 (7.07ms) python3 (564ms)

array([-0.15143764,  0.1180403 , -0.28701825])

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

Code 2.2 SigmoidCrossEntropyクラスの定義

class SigmoidCrossEntropy(LossLayer):
    def forward(self):
        self.y.d = 1 / (1 + np.exp(-self.x.d))
        y = self.y.d.reshape(-1)
        self.size = y.shape[0]
        loss = np.c_[1 - y, y][np.arange(self.size), self.t.d.reshape(-1)]
        self.loss.d = -np.sum(np.log(loss + 1e-7)) / self.size

    def backward(self):
        self.x.g = (self.y.d - self.t.d) / self.size

    def predict(self) -> float:
        return (self.x.d > 0).astype(int)

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

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

from ivory.core.model import sequential

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

[32] 2019-06-12 20:00:47 (22.9ms) python3 (622ms)

[<Input('SigmoidCrossEntropy.1.x', ((),)) at 0x1f3ebf94240>,
 <Output('SigmoidCrossEntropy.1.y', ((),)) at 0x1f3ebf942b0>,
 <Input('SigmoidCrossEntropy.1.t', ()) at 0x1f3ebf94208>,
 <Loss('SigmoidCrossEntropy.1.loss', ()) at 0x1f3ebf942e8>]

変数を設定した後、入力とターゲットを代入します。

layer.set_variables()
layer.set_data(x, t)

[33] 2019-06-12 20:00:47 (15.6ms) python3 (638ms)

順伝搬を行います。

model.forward()

[34] 2019-06-12 20:00:47 (7.73ms) python3 (645ms)

逆伝搬を行います。

model.backward()
layer.x.g

[35] 2019-06-12 20:00:47 (9.31ms) python3 (655ms)

array([-0.15143764,  0.1180403 , -0.28701825])

さて、この勾配が正しいかは、数値微分による勾配確認によって検証できます。入力データの第1要素を少しだけずらして、損失を求めます。

epsilon = 1e-4
layer.x.d[0] += epsilon
model.forward()
plus = model.loss

[36] 2019-06-12 20:00:47 (6.01ms) python3 (661ms)

逆方向にずらします。

layer.x.d[0] -= 2 * epsilon
model.forward()
minus = model.loss

[37] 2019-06-12 20:00:47 (5.03ms) python3 (666ms)

勾配は次式で得られます。

print((plus - minus) / (2 * epsilon))

[38] 2019-06-12 20:00:47 (10.0ms) python3 (676ms)

-0.15143761637581576

これまでの結果に一致しています。入力をもとに戻しておきます。

layer.x.d[0] += epsilon

[39] 2019-06-12 20:00:47 (31.2ms) python3 (707ms)

ある変数の全ての要素について数値微分による勾配を求めるメソッドnumerical_gradientが用意されています。

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

[40] 2019-06-12 20:00:47 (15.6ms) python3 (722ms)

[-0.15143762  0.11804028 -0.28701805]

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

2.1.3 時系列データの場合

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

x = np.random.randn(N, T)
t = np.random.randint(0, 2, (N, T))

layer.set_data(x, t)

[41] 2019-06-12 20:00:47 (6.03ms) python3 (728ms)

順伝搬を行います。

model.forward()
print(layer.loss.d)

[42] 2019-06-12 20:00:47 (6.00ms) python3 (734ms)

0.7857680503475944

逆伝搬を行います。

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

[43] 2019-06-12 20:00:48 (6.98ms) python3 (741ms)

[[ 0.01143075 -0.06636342  0.01849615 -0.0497973 ]
 [-0.026705    0.03135866 -0.07129697 -0.01323991]
 [ 0.03217591  0.04082025 -0.0730233  -0.02233509]]

数値微分で確かめてみます。

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

[44] 2019-06-12 20:00:48 (9.02ms) python3 (750ms)

[[ 0.01143075 -0.06636339  0.01849615 -0.04979728]
 [-0.02670499  0.03135866 -0.07129692 -0.01323991]
 [ 0.0321759   0.04082024 -0.07302324 -0.02233509]]

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

from ivory.utils.repository import import_module

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

[45] 2019-06-12 20:00:48 (31.3ms) python3 (782ms)

0.7857680503475944
[[ 0.01143075 -0.06636342  0.01849615 -0.0497973 ]
 [-0.026705    0.03135866 -0.07129697 -0.01323991]
 [ 0.03217591  0.04082024 -0.0730233  -0.02233509]]