6.5. Affine / Softmax レイヤの実装

6.5.1. Affineレイヤ

バッチ処理などで行列の積を使ったので、その計算を行うAffineレイヤを作成する

Affine

6.5.2. バッチ版Affineレイヤ

バッチ版の場合、入力\(X\)の要素数が増えたり減ったりする。がバイアス\(B\)の要素数は固定なので、このバイアス\(B\)の逆要素をどうやって計算するのかがカギとなる。

例えば、入力\(x\)のデータ数は10個だとしても、バイアス\(b\)は1個で、結果\(y\)は10個である。
しかし逆伝播は、入力\(x\)は10個に戻るが、バイアス\(b\)は1個に戻らなければならない。

まず順伝播の計算から。

[1]:
import sys, os
sys.path.append(os.path.abspath(os.path.join('..', 'sample')))
import numpy as np

X_dot_W = np.array([[0, 0, 0,], [10, 10, 10]])
B = np.array([1, 2, 3])

print(X_dot_W)
[[ 0  0  0]
 [10 10 10]]
[2]:
X_dot_W + B
[2]:
array([[ 1,  2,  3],
       [11, 12, 13]])

ここから、逆伝播を考える。

バイアス\(B\)は、入力\(X\)のそれぞれの要素に対して加算される。そのため、逆伝播の値がバイアスの要素に集約される必要がある。

まず、逆伝播による、前のノードの値を\(dY\)、計算したデータが2個だとすると

[3]:
dY = np.array([[1, 2, 3], [4, 5, 6]])
print(dY)
[[1 2 3]
 [4 5 6]]
次に、\(dY\)をバイアス\(B\)からの逆伝播として、\(dY\)の結果を集約する。
データは2個だが、そのデータ内のそれぞれの要素の合計を求める。
[4]:
dB = np.sum(dY, axis=0)
print(dB)
[5 7 9]
[5]:
class Affine:
    def __init__(self, W, b):
        """
        重みとバイアスを初期化します
        Attributes
        ----------
        W : numpy.array
            重み
        b : numpy.array
            バイアス
        x : numpy.array
            順伝播で入力されるx
        original_x_shape : numpy.array
            xのデータ数
        dW : numpy.array
            Wからの逆伝搬
        db : numpy.array
            bからの逆伝播
        """
        self.W =W
        self.b = b

        self.x = None
        self.original_x_shape = None
        # 重み・バイアスパラメータの微分
        self.dW = None
        self.db = None

    def forward(self, x):
        """
        順伝播の計算をします

        Parameters
        ----------
        x : numpy.array
            入力1

        Returns
        -------
        out : numpy.array
            計算結果
        """
        # テンソル対応
        self.original_x_shape = x.shape
        x = x.reshape(x.shape[0], -1)
        self.x = x

        out = np.dot(self.x, self.W) + self.b

        return out

    def backward(self, dout):
        """
        逆伝播の計算をします(乗算)

        Parameters
        ----------
        dout : int
            逆伝播で上流から伝わってきた微分

        Returns
        -------
        dx : int
            入力2の微分
        """
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)

        dx = dx.reshape(*self.original_x_shape)  # 入力データの形状に戻す(テンソル対応)
        return dx

6.5.2.1. x.reshape(x.shape[0], -1) の -1?

  • reshapeは、次元数を書くとその次元数に合わせて変換した行列を作ってくれる

  • 何かの次元数と-1を組み合わせると、指定してくれた次元数以外の次元を自動的に生成してくれる

[6]:
a = np.arange(24) # 0~23の一次元配列
print(a)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
[7]:
a.reshape([2, -1]) # (2, 12)のshape
[7]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
       [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]])
[8]:
a.reshape([2, 0]) # (2, 0)のshape…はつくれない
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-8-b6375f3eef96> in <module>
----> 1 a.reshape([2, 0]) # (2, 0)のshape…はつくれない

ValueError: cannot reshape array of size 24 into shape (2,0)
[9]:
a.reshape([3, -1]) # (3, 8)のshape
[9]:
array([[ 0,  1,  2,  3,  4,  5,  6,  7],
       [ 8,  9, 10, 11, 12, 13, 14, 15],
       [16, 17, 18, 19, 20, 21, 22, 23]])

6.5.3. Softmax-with-Lossレイヤ

Softmax-with-Lossレイヤーは、Softmax関数と交差エントロピー誤差(cross entropy error)の二つが合体したレイヤー。

Softmax関数は、出力層で使われる関数で、n個の出力結果の合計が「1.0」になるように正規化してくれます。(3章の話)

  • ニューラルネットワークの「学習」フェーズで使う

  • 推論では、結果が高い、低いだけわかればよかった

  • 学習では、高い、低いの正しいのかを機械的に比較する必要があるので使う

一方、交差エントロピー誤差(cross entropy error)は、「教師データが示す答え」と「学習によって出力した答え」を比較して、「学習によって出力した答え」がどれだけ間違っているのかを示すための方法です。(4章)

交差エントロピー誤差を出す前に正規化しないと、わけわかんない結果になるよ!たぶん!

(計算グラフについては時間がないので省略しまする…大事なところだけど)

Softmax関数の逆伝播は、前の交差エントロピー誤差によって出た結果をキレイに伝えてくれる

[10]:
class SoftmaxWithLoss:
    def __init__(self):
        """
        損失、softmaxの出力、教師データを入れる場所を作ります
        Attributes
        ----------
        loss : numpy.array
            損失
        y : numpy.array
            softmaxの出力
        t : numpy.array
            教師データの答え
        """
        self.loss = None
        self.y = None # softmaxの出力
        self.t = None # 教師データ

    def forward(self, x, t):
        """
        順伝搬の計算をします

        Parameters
        ----------
        x : numpy.array
            前のレイヤーから出力した結果
        t : numpy.array
            教師データの答え
        Returns
        -------
        dx : float
            損失
        """
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)

        return self.loss

    def backward(self, dout=1):
        """
        逆伝播の計算をします

        Parameters
        ----------
        dout : int
            正規化の値(1)

        Returns
        -------
        dx : int
            損失から計算した差
        """
        batch_size = self.t.shape[0]
        if self.t.size == self.y.size: # 教師データがone-hot-vectorの場合
            dx = (self.y - self.t) / batch_size
        else:
            dx = self.y.copy()
            dx[np.arange(batch_size), self.t] -= 1
            dx = dx / batch_size

        return dx