バッチ正規化

対象となる層の活性を正規化し、入力の偏りによる内部共変量シフトを抑制する。
重みの初期値をあまり気にしなくてもよくなり、過学習も抑制される。

入力の平均値と分散を求め、各入力に対して、平均値を引いた後、標準偏差で割る(標準化)。その後、スケール、平行移動して、出力の分布を作成する。

\( \displaystyle \mu = \frac{1}{n} \sum^n_{i=1}x_i \)
\( \displaystyle \sigma ^2 = \frac{1}{n} \sum^2_{i=1}(x_i-\mu)^2\)
\( \displaystyle \hat{x_i} = \frac{x_i-\mu}{\sqrt{\sigma^2+\epsilon}} \)
\( \displaystyle y_i = \gamma\hat{x_i} + \beta \)

\( y_i \)が出力の分布。\( \gamma \)と\( \beta \)は重みと共に学習されていくパラメータなので、推論時にはこれらの保存も必要。

初期値を気にしなくて済むのは良いですね。重みが無駄に偏る心配がないのは良いかと。その分、\( \gamma \)と\( \beta \)を保存しておかなくてはならないのが面倒ですね。

Adam

重みの更新方法の一つ。
MomemtumとRMSPropを組み合わせた方法。
勾配の2乗の移動平均だけでなく、勾配の移動平均も保持する。

\( m_t = \rho _1 m_{t-1} + (1-\rho _1)\frac{\partial L}{\partial W_t} \)
\( v_t = \rho _2 v_{t-1} + (1-\rho _2)\frac{\partial L}{\partial W_t} \odot \frac{\partial L}{\partial W_t} \)
\( \hat{m_t} = \frac{m_t}{1-\rho ^2_1} \)
\( \hat{v_t} = \frac{v_t}{1-\rho ^t_2}\)

\( W_{t+1} = W_t – \eta\frac{1}{\sqrt{\hat{v_t}} +\delta} \odot \hat{m_t} \)

\( \rho _1 \)、\( \rho _2 \)は共に減衰率で1に近いハイパーパラメータ。\( t \)はステップ数。\( \odot \)はアダマール積。\( \delta \)は0割り防止の小さい数。

正則化

計算した重みに制限を加える事で過学習を抑制する方法。
weight decay, dropout, early stoppingがそれに当たる。
過学習を抑制する工夫という意味では、重みにノイズを加えたりするのも含まれるのかな?

荷重減衰 (Weight decay)
損失関数が大きくなり過ぎないよう、荷重を減らす作用をする項を加える。
\( L = L + \frac{1}{2} \lambda \sum^{n}_{l=1} \sum_{i, j} (w^{l}_{ij})^2 \)

\( L \)は損失関数、\( n \)はレイヤの数、\( w^{l}_{ij}\)は\( l \)番目の層の重み行列。\( \lambda \)は手入力するパラメータで、小さい値を入れる。

重み\( W \)に対する損失関数の勾配は\( \frac{\partial L}{\partial W} \)で、重みの更新は\( W \gets W – \eta \frac{\partial L}{\partial W} \)だったので、正則化を考慮すると、\( W \gets W – \eta (\frac{\partial L}{\partial W}+\lambda W) \)となる。\( W\)が正の場合は更新された\( W \)は小さく、負の場合は大きくなる。

…けど。重みを小さくすればなぜ過学習が防げるのかがいまいち分からんね。過学習はトレーニングデータの特徴を過度に認識すると現れるので、トレーニングデータが少ないと起こりやすい。”過度に認識”の部分が”重みを大きく”の部分に対応すると考えれば、そういうものか、と思う気もするけど、認識した学習を訛らせる訳なので、効率は悪くなるのでは?だんだん重みが小さくなると、今度は小さくなり過ぎて学習がサチってしまわないかな?

最尤推定法

母集団の分布の形が分かっている(正規分布している、など)が、その母数が未知である(平均値や分散は未知)の場合、n個の測定値から母数を推定する方法。

尤度関数 (ゆうどかんすう, Likelihood function)

\( \displaystyle L(\theta) =L(\theta; x_1, x_2, x_3, … ,x_n) = \prod_{i=1}^n p(x_i;\theta) \)

確率密度関数が\( p(x, \theta) \)で与えられ、母集団から\( n \)個の標本をランダムで抽出して、その値を\( {x_1, x_2, x_3, …, x_n} \)と表記される時の尤度関数は\( L(θ)=L(\theta; x_1, x_2, x_3, …, x_n) = \prod_{i=1}^{n} p(x_i;\theta) \)と表します。\( {x_1, x_2, x_3, …, x_n} \)はサンプリングされた確定値で、未知数は\( \theta \)の方です。

最尤推定法 (さいゆうすいていほう, Maximum likelihood estimate)

\( \theta \)の最もそれっぽい値、\( \hat{\theta} \)を推定する方法。尤度関数の\( \theta \)を変えていくと当然ながら\( L(\theta) \)の計算値は変化していきます。右辺は\( {x_1, x_2, x_3, …, x_n} \)が観測される確率なので、\( L(\theta) \)の関数と合っていなければ小さく、合っていれば大きくなります。従って\( L(\theta) \)の最大値を取れば、それが一番もっともらしい\( \theta = \hat{\theta} \)となります。最大値は凸関数であれば微分が0になる点が最大値なので、

\( \displaystyle \frac{\partial}{\partial \theta} L(\theta) = 0 \)

を解けばよい事になります。積の微分は計算が大変なので、

\( \displaystyle \frac{\partial}{\partial \theta} l(\theta) = \frac{\partial}{\partial \theta} \log L(\theta) = 0 \)

を代わりに解いても、同じ\( \hat{\theta} \)が得られます。

母集団が、平均値\( \mu \)、分散\( \sigma^2 \)であり、\( \sigma \)が既知で\( \mu \)を推定する場合、

\( \displaystyle \frac{\partial}{\partial \mu} l(\mu) = \frac{1}{\sigma^2}\sum^{n}_{i=1}(x_i-\mu) = 0 \)

を解けば良いので、

\( \displaystyle \mu = \frac{1}{n} \sum^{n}_{i=1}x_i \)

となります。

Softmax-with-Lossレイヤ

Softmax-with-Lossレイヤを実装

def softmax(x):
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 
    x = x - np.max(x)
    return np.exp(x) / np.sum(np.exp(x))

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)
        return self.loss

    def backward(self, dy=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

Softmaxレイヤは入力された値を正規化し、出力の和が1になるようにします。
入力に対してSoftmaxで処理し、誤差を求める関数で、出力層に配置します。

\( y(k) = \frac{\exp(a_k)}{\sum^n_{i=1}\exp(a_i)} \)

多値の識別問題に使います。
正規化するだけなので、重みの順序は変わりません。

損失関数として交差エントロピー誤差を使います。
logの中身が0にならないよう、1e-7といった小さい値を足しておきます。
損失関数を使う事により、学習の進捗が浮動小数点で扱えるようになり、離散的な問題の最適化においても、プラトーに遭遇しにくくなるようです。

ニューラルネットワークはとにかく0, 1のような離散数を出さないようにする工夫が多いですね。

Sigmoidレイヤ

Sigmoidレイヤを実装

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

class Sigmoid:
    def __init__(self):
        self.y = None

    def forward(self, x):
        out = sigmoid(x)
        self.y = y
        return y

    def backward(self, dy):
        dx = dy * (1.0 - self.y) * self.y
        return dx

不思議な形をしたシグモイド。
二値分類問題ではシグモイド関数を使います。
恒等関数でも良いような気もするけど、非線形関数を使う事が機械学習のポイントです。

変な形をしているだけに、使い方が難しいんじゃないかと思うのです。
これも勉強を進めていけば、しっかりと使い道が分かるのようになるのだろうか??

ReLUレイヤ

ReLUレイヤを実装

class ReLU:
    def __init__(self):
        self.mask = None

    def forward(self, x):
        self.mask = (x <= 0)
        y = x.copy()
        y[self.mask] = 0
        return y

    def backward(self, dy):
        dy[self.mask] = 0
        dx = dy
        return dx

maskの使い方が面白いです。
ReLUはforwardではxが0以下の時は0を返し、0より大きいときは恒等関数になります。
通常はif文を使って書けばよいような気がしますが、maskを使うと上記のように書けます。
x<=0の時maskはtrueになって、それ以外の時はfalseになるので、yにxをいれておいて、maskがtrueの部分だけ0にすればよいわけですね。 ... if文で良いのでは ...? ところでReLUってどう読むんでしょうか? スペルアウトはRectified Linear Unitなのだそうです。 言葉を作るのは勝手ですが、読み方までしっかり決めておいてほしいですよね。

Affineレイヤ

Affineレイヤを実装。

class Affine:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None

    def forward(self, x):
        self.x = x
        return np.dot(self.x, self.W) + self.b

    def backward(self, dy):
        dx = np.dot(dy, self.W.T)
        self.dW = np.dot(self.x.T, dy)
        self.db = np.sum(dy, axis=0)
        return dx

Affine変換とかあるじゃないですか。Affineで変な響きだし、Affineてなによ、と思ったら、一次関数のようです。

Ax+b

線形変換かよ。
ただ、ニューラルネットワークでは良く使うようです。
Wはウエイト(傾き)、xは入力、bはバイアス(切片)に対応します。
全て多次元配列ですので要素数の一致が大事ですね。

クラスAffineの中に、forward関数を定義します。
xを引き数にして、xとWの内積にbを足しますので、xの列数、Wの列数、xとWの内積がbの行列数と一致している必要があります。

同様にbackward関数は誤差逆伝播法で使うので定義します。

 dL/dx = dL/dy dot W.T
 dL/dW = X.T dot dL/dy
 dL/db = sum(dL/dy)

ですね。

早速試してみます。

x = np.array([[1, 2]])
W = np.array([[4, 5, 6],
              [7, 8, 9]])
b = np.array([[10, 11, 12]])

affine = Affine(W, b)
print("forward=", affine.forward(x))

dy = np.array([[1, 2, 3]])
dx = affine.backward(dy)
print("dx=", dx)

forward= [[28 32 36]]
dx= [[32 50]]

forwardはx dot W + bなので、
(1×4+2×7+10, 1×5+2×8+11, 1×6+2×9+12) = (28, 32, 36)
でOK。

backwardはdyとして(1, 2, 3)が返ってきた場合、
(1×4+2×5+3×6, 1×7+2×8+3×9) = (32, 50)
なのでOKですかね。

Multiply関数

Multiply関数を実装。

class MultiLayer:
    def forward(self, x, y):
        self.x = x
        self.y = y
        return x * y
    def backward(self, dLda):
        dLdx = dLda * y
        dLdy = dLda * x
        return dLdx, dLdy

掛け算の関数です。
forwardはただ掛け算するだけですが、backwardの時に使うのでxとyを保存しておきます。

最終結果Lに対するxの勾配、yの勾配は、
dL/dx = dL/da x da/dx = dL/da x y
dL/dy = dL/da x da/dy = dL/da x x
なので、一つ上流の勾配であるdL/daに、xとyを対応させれば良いという事になります。

では試してみましょう。

mul = MultiLayer()
x = np.array([[1],[2]])
y = np.array([[3],[4]])
dLda = np.array([5])
print("forward = ", mul.forward(x, y))
dLdx , dLdy = mul.backward(dLda)
print("dLdx = ", dLdx)
print("dLdy = ", dLdy)

>> forward = [[3] [8]]
   dLdx = [[15] [20]]
   dLdy = [[ 5] [10]]

forwardは(1×3, 2×4) = (3, 8)ですね。
backwardは1行目が(5×3, 5×4) = (15, 20)
2行目が(5×1, 5×2) = ( 5, 10)
ですね。

Add関数

Add関数を実装。

class AddLayer:
    def forward(self, x, y):
        return x + y
    def backward(self, dLda):
        dLdx = dLda * 1
        dLdy = dLda * 1
        return dLdx, dLdy

足し算の関数です。
forwardはただ加算して値を返すだけ。
x + y = a
で、最終結果Lに対するxの勾配、yの勾配を求めます。
dL/dx = dL/da x da/dx = dL/da x 1
dL/dy = dL/da x da/dy = dL/da x 1
なので、一つ上流の勾配であるdL/daをそのまま伝播させれば良いという事になります。

では試してみましょう。

add = AddLayer()
x = np.array([[1],[2]])
y = np.array([[3],[4]])
dLda = np.array([5])
print("forward = ", add.forward(x, y))
dLdx , dLdy = add.backward(dLda)
print("dLdx = ", dLdx)
print("dLdy = ", dLdy)

>> forward = [[4] [6]]
   dLdx = [5]
   dLdy = [5]

できた。