ブログ

自動微分・誤差逆伝播の理論と応用④:Attentionとグラフニューラルネットワーク

自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事ではAttention処理とグラフニューラルネットワーク(GNN; Graph Neural Network)の実装について取り扱いました。
作成にあたっては「ゼロから作るDeep Learning②」の第$8$章「Attention」の内容を元に大幅に改変を行いました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

・GNN入門

MessagePassing

NumPy.repeat

AttentionのようなMessagePassing処理を取り扱う際に係数のベクトルへの掛け算などを行列積で取り扱うのはなかなか難しいので、NumPy.repeatを用いることで要素積の計算を行うというのも一つの手段である。NumPy.repeatはたとえば下記のように使用することができる。

import numpy as np

x1 = np.array([[1., 2., 3.]])

print(x1.shape)
print(x1.repeat(2, axis=0).shape)
print(x1.repeat(2, axis=1).shape)
print(x1.repeat(2, axis=0))
print(x1.repeat(2, axis=1))

・実行結果

(1, 3)
(2, 3)
(1, 6)
[[1. 2. 3.]
 [1. 2. 3.]]
[[1. 1. 2. 2. 3. 3.]]

上記は$2$次元配列の場合であるが、$3$次元の場合も下記のように繰り返し操作を行える。

x2 = np.array([[[1., 2., 3.]]])

print(x2.shape)
print(x2.repeat(2, axis=0).shape)
print(x2.repeat(2, axis=1).shape)
print(x2.repeat(2, axis=2).shape)
print(x2.repeat(2, axis=0))
print(x2.repeat(2, axis=1))
print(x2.repeat(2, axis=2))

・実行結果

(1, 1, 3)
(2, 1, 3)
(1, 2, 3)
(1, 1, 6)
[[[1. 2. 3.]]

 [[1. 2. 3.]]]
[[[1. 2. 3.]
  [1. 2. 3.]]]
[[[1. 1. 2. 2. 3. 3.]]]

Attentionの実装

Attentionの基本処理

「ゼロから作るDeepLearning②」ではAttentionの重み付け和の計算にあたって、下記のようなWeightSumクラスを定義する。

class WeightSum:
    def __init__(self):
        self.params, self.grads = [], []
        self.cache = None

    def forward(self, hs, a):
        N, T, H = hs.shape
        ar = a.reshape(N, T, 1).repeat(H, axis=2)
        t = hs * ar
        c = np.sum(t, axis=1)
        self.cache = (hs, ar)
        return c

    def backward(self, dc):
        hs, ar = self.cache
        N, T, H = hs.shape
        dt = dc.reshape(N, 1, H).repeat(T, axis=1)
        dar = dt * hs
        dhs = dt * ar
        da = np.sum(dar, axis=2)
        return dhs, da

順伝播の実行例

a = np.array([[1., 0.5], [0.2, 0.5]])
hs = np.ones([2, 2, 3])

calc_weight_sum = WeightSum()
c = calc_weight_sum.forward(hs, a)
print(c)

・実行結果

[[1.5 1.5 1.5]
 [0.7 0.7 0.7]]

逆伝播の実行例

dc = np.ones([2, 3])
dhs, da = calc_weight_sum.backward(dc)

print(dhs.shape)
print(dhs)
print("===")
print(da.shape)
print(da)

・実行結果

(2, 2, 3)
[[[1.  1.  1. ]
  [0.5 0.5 0.5]]

 [[0.2 0.2 0.2]
  [0.5 0.5 0.5]]]
===
(2, 2)
[[3. 3.]
 [3. 3.]]

MessagePassingの実装

隣接行列:グラフの定義

$5$ノードのグラフの隣接行列は下記のように定義することができる。

adj_mat = np.array([[1, 0, 1, 1, 0], [0, 1, 1, 0, 1], [1, 1, 1, 0, 1], [1, 0, 0, 1, 1], [0, 1, 1, 1, 1]])
print(adj_mat)
print(adj_mat==adj_mat.T)

・実行結果

[[1 0 1 1 0]
 [0 1 1 0 1]
 [1 1 1 0 1]
 [1 0 0 1 1]
 [0 1 1 1 1]]
[[ True  True  True  True  True]
 [ True  True  True  True  True]
 [ True  True  True  True  True]
 [ True  True  True  True  True]
 [ True  True  True  True  True]]

ここでは無向グラフを取り扱うので、隣接行列が対称行列であることも合わせて確認しておくと良い。

MessagePassingクラスの実装

WeightSumクラスを改変することで下記のようにMessagePassingクラスの実装を行なった。

class MessagePassing:
    def __init__(self, adj_mat):
        self.params, self.grads = [], []
        self.graph = adj_mat
        self.cache = None

    def forward(self, h):
        N, T, H = h.shape
        m_t = np.zeros_like(h)
        for i in range(self.graph.shape[0]):
            ar = self.graph[i,:].reshape(1, T, 1).repeat(N, axis=0).repeat(H, axis=2)
            t = h * ar
            m_t[:, i, :] = np.sum(t, axis=1)
        self.cache = h
        return m_t

    def backward(self, dc):
        h = self.cache
        N, T, H = h.shape
        dh = np.zeros_like(h)
        for i in range(self.graph.shape[0]):
            ar = self.graph[i, :].reshape(1, T, 1).repeat(N, axis=0).repeat(H, axis=2)
            dt = dc.reshape(N, 1, H).repeat(T, axis=1)
            dh[:, i, :] = np.sum(dt * ar, axis=1)
        return dh

上記のMessagePassingクラスではWeightSumクラスの重み付け和の計算にあたって、隣接行列を用いるように改変を行なった。

MessagePassingクラスの実行例

■ 順伝播

順伝播の計算は下記のように実行することができる。

calc_mp = MessagePassing(adj_mat)

h = np.ones([2, 5, 3])
h[0,0,0] = 5
m_t = calc_mp.forward(h)

print(m_t.shape)
print(m_t)

・実行結果

(2, 5, 3)
[[[7. 3. 3.]
  [3. 3. 3.]
  [8. 4. 4.]
  [7. 3. 3.]
  [4. 4. 4.]]

 [[3. 3. 3.]
  [3. 3. 3.]
  [4. 4. 4.]
  [3. 3. 3.]
  [4. 4. 4.]]]

上記の結果が正しいかどうかは、下記のような計算を行うことで確認できる。

print(np.sum(h[0, :, 0] * adj_mat[0, :]))
print(np.sum(h[0, :, 0] * adj_mat[2, :]))
print(np.sum(h[0, :, 0] * adj_mat[3, :]))

・実行結果

7.0
8.0
7.0

■ 逆伝播

dc = np.ones([2, 3])
dh = calc_mp.backward(dc)
print(dh)

・実行結果

[[[3. 3. 3.]
  [3. 3. 3.]
  [4. 4. 4.]
  [3. 3. 3.]
  [4. 4. 4.]]

 [[3. 3. 3.]
  [3. 3. 3.]
  [4. 4. 4.]
  [3. 3. 3.]
  [4. 4. 4.]]]

GNNの実装

GNNのレイヤーの実装

クラスの実装

Affineクラス

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

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

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

GNN_Layerクラス

class GNN_Layer:
    def __init__(self, W, b, adj_mat):
        self.W = W
        self.b = b
        self.dW = np.zeros_like(W)
        self.db = np.zeros_like(b)
        self.h = None
        self.graph = adj_mat

    def forward(self, h):
        N, T, H = h.shape
        self.h = h

        self.affines = []
        
        self.calc_mp = MessagePassing(self.graph)
        m_t = self.calc_mp.forward(h)
        h_next = np.zeros_like(h)
        for i in range(self.graph.shape[0]):
            affine = Affine(self.W, self.b)
            h_next[:, i, :] = affine.forward(m_t[:, i, :])
            self.affines.append(affine)
            
        return h_next

    def backward(self, dh_next):
        N, T, H = self.h.shape
        
        dh = np.zeros_like(self.h)
        for i in range(self.graph.shape[0]):
            dh[:, i, :] = self.affines[i].backward(dh_next[:, i, :])
            self.dW += self.affines[i].dW
            self.db += self.affines[i].db
            
        dh = self.calc_mp.backward(dh)
        return dh

使用例

■ 順伝播

N, T, H = 2, 5, 3

h = np.ones([2, 5, 3])
W = np.ones([H, H])
b = np.ones(H)

adj_mat = np.array([[1, 0, 1, 1, 0], [0, 1, 1, 0, 1], [1, 1, 1, 0, 1], [1, 0, 0, 1, 1], [0, 1, 1, 1, 1]])

gnn_layer = GNN_Layer(W, b, adj_mat)
h_next = gnn_layer.forward(h)

print(h_next)

・実行例

[[[10. 10. 10.]
  [10. 10. 10.]
  [13. 13. 13.]
  [10. 10. 10.]
  [13. 13. 13.]]

 [[10. 10. 10.]
  [10. 10. 10.]
  [13. 13. 13.]
  [10. 10. 10.]
  [13. 13. 13.]]]

■ 逆伝播

dh_next = np.ones([2, 5, 3])

dh = gnn_layer.backward(dh_next)

print(dh)

・実行結果

[[[ 9.  9.  9.]
  [ 9.  9.  9.]
  [12. 12. 12.]
  [ 9.  9.  9.]
  [12. 12. 12.]]

 [[ 9.  9.  9.]
  [ 9.  9.  9.]
  [12. 12. 12.]
  [ 9.  9.  9.]
  [12. 12. 12.]]]

$2$層GNNの実装

関数・クラスの実装

■ 活性化関数・誤差関数

def softmax(a):    # a is 2D-Array
    c = np.max(a, axis=1)
    exp_a = np.exp(a.T-c)
    sum_exp_a = np.sum(exp_a, axis=0)
    y = (exp_a / sum_exp_a).T
    return y

def softmax_3D(a):    # a is 3D-Array
    N, L, H = a.shape
    c = np.max(a, axis=2)
    exp_a = np.exp(a-c.reshape([N, L, 1]).repeat(H, axis=2))
    sum_exp_a = np.sum(exp_a, axis=2)
    y = exp_a / sum_exp_a.reshape([N, L, 1]).repeat(H, axis=2)
    return y

def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t * np.log(y+delta))

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

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

    def backward(self, dout):
        dx = dout * self.y * (1-self.y)
        return dx

class Aggregate:
    def __init__(self):
        self.h = None
        self.y = None

    def forward(self, h):
        self.h = h
        self.y = np.sum(h, axis=1)
        return self.y

    def backward(self, dy):
        N, T, H = self.h.shape
        dh = dy.reshape(N, 1, H).repeat(T, axis=1)
        return dh

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, dout=1.):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

途中で用いたソフトマックス関数は下記のように定義される。
$$
\large
\begin{align}
y_k &= \mathrm{Softmax}(x_k) = \frac{\exp{(x_k)}}{S} \\
S &= \sum_{j} \exp{(x_j)}
\end{align}
$$

上記の$x_k$に関する偏微分は分数関数の微分の公式を元に下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial y_k}{\partial x_k} &= \frac{\exp{(x_k)}S – \exp{(x_k)} \cdot \exp{(x_k)}}{S^{2}} \\
&= \frac{\exp{(x_k)}(S – \exp{(x_k)})}{S^{2}} \\
&= \frac{\exp{(x_k)}}{S} \cdot \left( 1 – \frac{\exp{(x_k)}}{S} \right) = y_k(1-y_k)
\end{align}
$$

上記に基づいてSoftmaxクラスのbackwardメソッドの実装を行なった。

■ $2$層GNN

from collections import OrderedDict

class TwoLayerGNN:
    def __init__(self, input_size, hidden_size, output_size, adj_mat, weight_init_std=0.01):
        self.params = {}
        self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params["b1"] = np.zeros(hidden_size)
        self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params["b2"] = np.zeros(output_size)

        # generate layers
        self.layers = OrderedDict()
        self.layers["GNN_layer1"] = GNN_Layer(self.params["W1"], self.params["b1"], adj_mat)
        self.layers["Softmax1"] = Softmax()
        self.layers["GNN_layer2"] = GNN_Layer(self.params["W2"], self.params["b2"], adj_mat)
        self.layers["Aggregate"] = Aggregate()
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def calc_gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = self.lastLayer.backward(1.)
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # output
        grads = {}
        grads["W1"] = self.layers["GNN_layer1"].dW
        grads["b1"] = self.layers["GNN_layer1"].db
        grads["W2"] = self.layers["GNN_layer2"].dW
        grads["b2"] = self.layers["GNN_layer2"].db

        return grads

実行例

softmax_3D関数

x = np.ones([2,5,3])
x[:, :, 1] = 2
softmax_3D(x)

・実行結果

array([[[0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156]],

       [[0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156],
        [0.21194156, 0.57611688, 0.21194156]]])

■ 学習の実行

np.random.seed(0)

alpha = 0.1
adj_mat = np.array([[1, 0, 1, 1, 0], [0, 1, 1, 0, 1], [1, 1, 1, 0, 1], [1, 0, 0, 1, 1], [0, 1, 1, 1, 1]])

x = np.ones([2, 5, 2])
x[0, :, :1] = -1.
t = np.array([[1., 0.], [0., 1.]])

network = TwoLayerGNN(2, 2, 2, adj_mat)

for i in range(10):
    grad = network.calc_gradient(x, t)

    for key in ("W1", "b1", "W2", "b2"):
        network.params[key] -= alpha * grad[key]

    if (i+1)%2==0:
        print(softmax(network.predict(x)))

・実行結果

[[9.99882931e-01 1.17068837e-04]
 [9.99882862e-01 1.17138203e-04]]
[[2.09556970e-08 9.99999979e-01]
 [3.65517109e-09 9.99999996e-01]]
[[9.99966650e-01 3.33497863e-05]
 [1.24346800e-12 1.00000000e+00]]
[[1.00000000e+00 1.81375702e-28]
 [1.59424189e-14 1.00000000e+00]]
[[1.00000000e+00 1.22993158e-51]
 [1.62446544e-16 1.00000000e+00]]

■ 推論

x = np.array([[[-1, 1], [-1, 1], [-0.5, 0.3], [-1.2, 0.8], [-0.9, 1.]]])
print(network.predict(x))
print(softmax(network.predict(x)))

・実行結果

[[ 58.68030203 -58.54451771]]
[[1.00000000e+00 1.23000717e-51]]

自動微分・誤差逆伝播の理論と応用③:RNN(Recurrent Neural Network)

自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事ではリカレントニューラルネットワーク(RNN; Recurrent Neural Network)の実装について取り扱いました。
作成にあたっては「ゼロから作るDeep Learning②」の第$5$章「リカレントニューラルネットワーク」の内容を主に参照しました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

RNNモジュールの実装

概要

RNNにおける基本的な再帰処理は下記のような数式で表すことができる。
$$
\large
\begin{align}
\mathbf{h}_{t} &= \tanh{(\mathbf{h}_{t-1} W_{h} + \mathbf{x}_{t} W_{x} + \mathbf{b})} \\
\mathbf{h}_{t-1}^{\mathrm{T}}, \, \mathbf{h}_{t}^{\mathrm{T}} & \in \mathbb{R}^{H}, \, W_{h} \in \mathbb{R}^{H \times H}, \, \mathbf{x}_{t}^{\mathrm{T}} \in \mathbb{R}^{D}, \, W_{x} \in \mathbb{R}^{D \times H}, \, \mathbf{b}^{\mathrm{T}} \in \mathbb{R}^{H}
\end{align}
$$

上記はバッチサイズが$N=1$の場合の表記である。同様にバッチサイズ$N$の場合は下記のような数式で再帰処理を表せる。
$$
\large
\begin{align}
\mathbf{h}_{t} &= \tanh{(\mathbf{h}_{t-1} W_{h} + \mathbf{x}_{t} W_{x} + \mathbf{b})} \\
\mathbf{h}_{t-1}, \, \mathbf{h}_{t} & \in \mathbb{R}^{N \times H}, \, W_{h} \in \mathbb{R}^{H \times H}, \, \mathbf{x}_{t} \in \mathbb{R}^{N \times D}, \, W_{x} \in \mathbb{R}^{D \times H}, \, \mathbf{b} \in \mathbb{R}^{N \times H}
\end{align}
$$

上記の微分はAffine変換の微分と同様に取り扱えば良い。

$\tanh{(x)}$の定義と微分

ハイパボリックタンジェント関数$\tanh{(x)}$は下記のように定義される。
$$
\large
\begin{align}
\tanh{(x)} = \frac{e^{x} – e^{-x}}{e^{x} + e^{-x}}
\end{align}
$$

上記の$x$に関する微分は分数関数の微分の公式に基づいて下記のように得られる。
$$
\large
\begin{align}
(\tanh{(x)})’ &= \frac{(e^{x} + e^{-x})^{2} – (e^{x} – e^{-x})^{2}}{(e^{x} + e^{-x})^{2}} \\
&= 1 – \frac{(e^{x} – e^{-x})^{2}}{(e^{x} + e^{-x})^{2}} \\
&= 1 – \tanh{(x)}
\end{align}
$$

実装と使用例

class RNN:
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.cache = None

    def forward(self, x, h_prev):
        Wx, Wh, b = self.params
        t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
        h_next = np.tanh(t)
        self.cache = (x, h_prev, h_next)
        return h_next

    def backward(self, dh_next):
        Wx, Wh, b = self.params
        x, h_prev, h_next = self.cache
        
        dt = dh_next * (1. - h_next**2)
        db = np.sum(dt, axis=0)
        dWh = np.dot(h_prev.T, dt)
        dh_prev = np.dot(dt, Wh.T)
        dWx = np.dot(x.T, dt)
        dx = np.dot(dt, Wx.T)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        return dx, dh_prev

順伝播の計算

下記のようにRNNクラスを用いることで順伝播の計算を行うことができる。

Wx = np.array([[-2., 1., 3.], [2., -3., 1.]])
Wh = np.array([[1., 2., -5.], [2., -5., 7.], [1., 5., 2]])
b = np.array([-2., -2., -5.])

x = np.array([[1., 2.]])
h_prev = np.array([[2., 1., 2.]])
dh_next = np.array([1., 1., 1.])

rnn = RNN(Wx, Wh, b)
h_next = rnn.forward(x, h_prev)

print(h_next)

・実行結果

[[0.99998771 0.96402758 0.76159416]]

上記では順伝播の計算を行ったが、計算の途中経過が正しいことは下記より確認できる。

t = np.dot(h_prev, Wh) + np.dot(x, Wx) + b
h_next = np.tanh(t)

print(np.dot(h_prev, Wh))
print(np.dot(x, Wx))
print(b)
print(t)
print(h_next)

・実行結果

[[6. 9. 1.]]
[[ 2. -5.  5.]]
[-2. -2. -5.]
[[6. 2. 1.]]
[[0.99998771 0.96402758 0.76159416]]

上記の計算は下記の数式の計算と同様である。
t = np.dot(h_prev, Wh) + np.dot(x, Wx) + bの計算
$$
\begin{align}
& H_{t-1} W_h + X W_x + B \\
&= \left( \begin{array}{ccc} 2 & 1 & 2 \end{array} \right)\left( \begin{array}{ccc} 1 & 2 & -5 \\ 2 & -5 & 7 \\ 1 & 5 & 2 \end{array} \right) + \left( \begin{array}{cc} 1 & 2 \end{array} \right)\left( \begin{array}{ccc} -2 & 1 & 3 \\ 2 & -3 & 1 \end{array} \right) + \left( \begin{array}{ccc} -2 & -2 & 5 \end{array} \right) \\
&= \left( \begin{array}{ccc} 6 & 9 & 1 \end{array} \right) + \left( \begin{array}{ccc} 2 & -5 & 5 \end{array} \right) + \left( \begin{array}{ccc} -2 & -2 & -5 \end{array} \right) \\
&= \left( \begin{array}{ccc} 6 & 2 & 1 \end{array} \right)
\end{align}
$$

h_next = np.tanh(t)の計算
$$
\large
\begin{align}
\tanh{(6)} &= \frac{e^{6}-e^{-6}}{e^{6}+e^{-6}} = 0.999 \cdots \\
\tanh{(2)} &= \frac{e^{2}-e^{-2}}{e^{2}+e^{-2}} = 0.964 \cdots \\
\tanh{(1)} &= \frac{e^{1}-e^{-1}}{e^{1}+e^{-1}} = 0.761 \cdots
\end{align}
$$

RNN全体の実装

実装

class TimeRNN:
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None
        self.h, self.dh = None, None

    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = xs.shape
        D, H = Wx.shape
        
        self.layers = []
        hs = np.empty((N, T, H), dtype="f")
        
        if self.h is None:
            self.h = np.zeros((N, H), dtype="f")

        for t in range(T):
            layer = RNN(*self.params)
            self.h = layer.forward(xs[:, t, :], self.h)
            hs[:, t, :] = self.h
            self.layers.append(layer)

        return hs

使用例

RNN全体の実装は基本的には前節のRNNクラスを複数回用いて構築を行う。以下、バッチ数が$N=2$、時系列の数が$T=2$、各入力ベクトルの次元が$D=3$、隠れ層の次元が$H=2$の場合について確認を行う。まず、入力に対応する配列を下記のように定義する。

xs = np.array([[[1., 2., 1.], [2., 1., 1.]], [[1., -2., 1.], [2., -1., 1.]]])
print(xs.shape)  # (N, T, D)

・実行結果

(2, 2, 3)

また、パラメータの$W_x, W_h, \mathbf{b}$はRNNクラスの各インスタンスで共有するので、下記のように定義できる。

Wx = np.array([[1., 1.], [2., -1], [-1., 1.]])
Wh = np.array([[1., -1.], [-1., 2.]])
b = np.array([[-1., 1.]]).repeat(2, axis=0)

print(Wx.shape)  # (D, H)
print(Wh.shape)  # (H, H)
print(b.shape)   # (N, H)

・実行結果

(3, 2)
(2, 2)
(2, 2)

このとき、TimeRNNクラスを用いて下記のようにRNNの順伝播を計算することができる。

rnns = TimeRNN(Wx, Wh, b)
hs = rnns.forward(xs)

print(xs.shape)
print(hs.shape)
print(hs[:, 1, :])

・実行結果

(2, 2, 3)
(2, 2, 2)
[[ 0.97729546  0.9982775 ]
 [-0.99932903  0.99999976]]

上記の結果は下記の実行結果に一致する。

h1 = np.tanh(np.dot(xs[:, 0, :], Wx) + b)
h2 = np.tanh(np.dot(h1, Wh) + np.dot(xs[:, 1, :], Wx) + b)
print(h2)

・実行結果

[[ 0.97729548  0.99827751]
 [-0.99932906  0.99999977]]

自動微分・誤差逆伝播の理論と応用②:Affine変換の自動微分とニューラルネットワークの実装

自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事ではAffine変換の自動微分とニューラルネットワーク(Neural Network)の実装について取りまとめました。
作成にあたっては「ゼロから作るDeep Learning」の第$5$章「誤差逆伝播法」の内容を主に参照しました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

Affine変換の自動微分

Affine変換の式定義と転置

MLP(Multi Layer Perceptron)の順伝播の計算はAffine変換の式を元に定義することができる。たとえば入力層を$\mathbf{x}$、計算に用いるパラメータを$W$、出力層を$\mathbf{y}$、バイアス項を$\mathbf{b}$とおくとき、Affine変換の式は下記のように表せる。
$$
\large
\begin{align}
\mathbf{y} = W \mathbf{x} + \mathbf{b} \quad (1.1)
\end{align}
$$

$\mathbf{y} \in \mathbb{R}^{d_x}, \, \mathbf{y} \in \mathbb{R}^{d_y}$であるとき、$(1.1)$式は$W \in \mathbb{R}^{d_y \times d_x}, \, \mathbf{b} \in \mathbb{R}^{d_y}$を用いて下記のように表すこともできる。
$$
\large
\begin{align}
\mathbf{y} &= W \mathbf{x} + \mathbf{b} \quad (1.1) \\
\left( \begin{array}{c} y_1 \\ \vdots \\ y_{d_y} \end{array} \right) &= \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_x} \\ \vdots & \ddots & \vdots \\ w_{d_y, 1} & \cdots & w_{d_y, d_x} \end{array} \right) \left( \begin{array}{c} x_1 \\ \vdots \\ x_{d_x} \end{array} \right) + \left( \begin{array}{c} b_1 \\ \vdots \\ b_{d_y} \end{array} \right) \quad (1.2)
\end{align}
$$

上記の式はオーソドックスな行列とベクトルの積の形式である一方で、統計学や機械学習では$\mathbf{x}$に複数サンプルを用いるにあたって、下記のような転置の形式を用いることが多い。
$$
\large
\begin{align}
\left( \begin{array}{c} y_1 \\ \vdots \\ y_{d_y} \end{array} \right)^{\mathrm{T}} &= \left[ \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_x} \\ \vdots & \ddots & \vdots \\ w_{d_y, 1} & \cdots & w_{d_y, d_x} \end{array} \right) \left( \begin{array}{c} x_1 \\ \vdots \\ x_{d_x} \end{array} \right) + \left( \begin{array}{c} b_1 \\ \vdots \\ b_{d_y} \end{array} \right) \right]^{\mathrm{T}} \\
\left( \begin{array}{ccc} y_1 & \cdots & y_{d_y} \end{array} \right) &= \left( \begin{array}{ccc} x_1 & \cdots & x_{d_x} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x, 1} & \cdots & w_{d_x, d_y} \end{array} \right) + \left( \begin{array}{ccc} b_1 & \cdots & b_{d_y} \end{array} \right) \quad (1.2)’
\end{align}
$$

$(1.2)’$式を元に$n$個の複数サンプルの入力と出力の対応の式を下記のように表すことができる。
$$
\begin{align}
\left( \begin{array}{ccc} y_{11} & \cdots & y_{1,d_y} \\ \vdots & \ddots & \vdots \\ y_{n,1} & \cdots & y_{n,d_y} \end{array} \right) &= \left( \begin{array}{ccc} x_{11} & \cdots & x_{1,d_x} \\ \vdots & \ddots & \vdots \\ x_{n,1} & \cdots & x_{n,d_x} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1, d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x, 1} & \cdots & w_{d_x, d_y} \end{array} \right) + \left( \begin{array}{ccc} b_{1} & \cdots & b_{d_y} \\ \vdots & \ddots & \vdots \\ b_{1} & \cdots & b_{d_y} \end{array} \right) \quad (1.3) \\
Y &= XW + B
\end{align}
$$

上記の$(1.3)$式の入力とパラメータの積の順序はデザイン行列と同様に理解しておくと良い。

入力・パラメータ・バイアスによる行列微分

多変数スカラー関数$\displaystyle L(Y) = \sum_{i=1}^{n} f(y_{i1}, \cdots , y_{i,d_y}) = \sum_{i=1}^{n} f(\mathbf{y}_{i})$を定義する。このとき関数$f$を$(1.3)$の表記に基づいて入力やパラメータについて行列微分を行う際の式について以下、確認を行う。

入力行列による行列微分

下記のように入力による行列微分$\displaystyle \frac{\partial L}{\partial X}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial X} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial x_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{1,d_x}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial x_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{n,d_x}} \end{array} \right)
\end{align}
$$

このとき、$\displaystyle \frac{\partial L}{\partial x_{ij}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial x_{ij}} &= \frac{\partial L}{\partial y_{i1}} \cdot \frac{\partial y_{i1}}{\partial x_{ij}} + \cdots + \frac{\partial L}{\partial y_{i,d_y}} \cdot \frac{\partial y_{i,d_y}}{\partial x_{ij}} \\
&= \sum_{k=1}^{d_y} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial x_{ij}} = \sum_{k=1}^{d_y} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} w_{jk} \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{i})}{\partial y_{i1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{i})}{\partial y_{i,d_y}} \end{array} \right) \left( \begin{array}{c} w_{j1} \\ \vdots \\ w_{j,d_y} \end{array} \right)
\end{align}
$$

上記より、$\displaystyle \frac{\partial L}{\partial X}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial X} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial x_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{1,d_x}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial x_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial x_{n,d_x}} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n,d_y}} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{d_x,1} \\ \vdots & \ddots & \vdots \\ w_{1,d_y} & \cdots & w_{d_x,d_y} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) \left( \begin{array}{ccc} w_{11} & \cdots & w_{1,d_y} \\ \vdots & \ddots & \vdots \\ w_{d_x,1} & \cdots & w_{d_x, d_y} \end{array} \right)^{\mathrm{T}} = \frac{\partial L}{\partial Y} W^{\mathrm{T}}
\end{align}
$$

パラメータ行列による行列微分

下記のようにパラメータ行列による行列微分$\displaystyle \frac{\partial L}{\partial W}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial w_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial w_{d_x,1}} & \cdots & \displaystyle \frac{\partial f}{\partial w_{d_x,d_y}} \end{array} \right)
\end{align}
$$

このとき、$\displaystyle \frac{\partial L}{\partial w_{jk}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial w_{jk}} &= \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial w_{jk}} \\
&= \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial w_{jk}} = \sum_{i=1}^{n} \frac{\partial f(\mathbf{y}_{i})}{\partial y_{ik}} x_{ij} \\
&= \left( \begin{array}{ccc} x_{1j} & \cdots & x_{nj} \end{array} \right) \left( \begin{array}{c} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1k}} \\ \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{nk}} \end{array} \right)
\end{align}
$$

上記より、$\displaystyle \frac{\partial L}{\partial W}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial w_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial w_{d_x,1}} & \cdots & \displaystyle \frac{\partial L}{\partial w_{d_x,d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} x_{11} & \cdots & x_{n1} \\ \vdots & \ddots & \vdots \\ x_{1,d_x} & \cdots & x_{n,d_x} \end{array} \right) \left( \begin{array}{ccc} \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{1})}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial f(\mathbf{y}_{n})}{\partial y_{n,d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} x_{11} & \cdots & x_{1,d_x} \\ \vdots & \ddots & \vdots \\ x_{n1} & \cdots & x_{n,d_x} \end{array} \right)^{\mathrm{T}} \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) = X^{\mathrm{T}} \frac{\partial L}{\partial Y}
\end{align}
$$

バイアス行列による行列微分

下記のようにパラメータ行列による行列微分$\displaystyle \frac{\partial L}{\partial B}$を定義する。
$$
\large
\begin{align}
\frac{\partial L}{\partial W} = \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \end{array} \right)
\end{align}
$$

このとき、$i$行目の$\displaystyle \frac{\partial L}{\partial b_{k}}$は微分の連鎖律に基づいて下記のように計算できる。
$$
\large
\begin{align}
\frac{\partial L}{\partial b_{k}} &= \frac{\partial L}{\partial y_{ik}} \cdot \frac{\partial y_{ik}}{\partial b_{k}} \\
&= \frac{\partial L}{\partial y_{ik}} \cdot 1 \\
&= \frac{\partial L}{\partial y_{ik}}
\end{align}
$$

上記より、$\displaystyle \frac{\partial L}{\partial B}$は下記のように表せる。
$$
\large
\begin{align}
\frac{\partial L}{\partial B} &= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial b_{1}} & \cdots & \displaystyle \frac{\partial L}{\partial b_{d_y}} \end{array} \right) \\
&= \left( \begin{array}{ccc} \displaystyle \frac{\partial L}{\partial y_{11}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{1,d_y}} \\ \vdots & \ddots & \vdots \\ \displaystyle \frac{\partial L}{\partial y_{n1}} & \cdots & \displaystyle \frac{\partial L}{\partial y_{n,d_y}} \end{array} \right) = \frac{\partial L}{\partial Y}
\end{align}
$$

バイアスパラメータはベクトルで定義されるので、勾配法の計算などの際は行方向に上記の和を計算し、パラメータのUpdateを行う。次項Affineクラスの17行目のself.db = np.sum(dout, axis=0)がバイアス行列の行方向の和の計算に対応する。

実装と使用例

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

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

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

上記のクラスは下記のように用いることができる。

# x = np.array([1., 2.]) # error
x = np.array([[1., 2.]])
# x = np.array([[1., 3.], [3., 5.], [5., 7.]]) # batch
W = np.array([[1., 2., 3.], [1., 2., 3.]])
b = np.array([1., 1., 1.])

mlp_layer = Affine(W, b)

# forward
y = mlp_layer.forward(x)

# backward
dout = np.ones(y.shape)
dx = mlp_layer.backward(dout)

print(y)
print(dx)

・実行結果

[[ 4.  7. 10.]]
[[6. 6.]]

入力に与えるx1Dではなく2Dで与える必要があることに注意が必要である。また、バイアス項のbNumPyのブロードキャストという機能に基づいて行われていることに着目しておくと良い。

ニューラルネットワーク

誤差関数と誤差逆伝播

ニューラルネットワークでは出力層のソフトマックスを計算したのちに、交差エントロピー(Cross Entropy)誤差関数を用いて学習を行う。中間層やパラメータなどの誤差関数の勾配を誤差逆伝播法に基づいて取得し、勾配法などを用いてパラメータの推定を行う。

実装と使用例

ソフトマックス関数と交差エントロピー誤差関数

ソフトマックス関数は下記のように実装できる。

def softmax(a):    # a is 2D-Array
    c = np.max(a, axis=1)
    exp_a = np.exp(a.T-c)
    sum_exp_a = np.sum(exp_a, axis=0)
    y = (exp_a / sum_exp_a).T
    return y

softmax関数の実装は「ゼロから作るDeepLearning」の$3$章の実装は1Dの配列用であるので、2D用に改変を行った。softmax関数は下記のように実行することができる。

x1 = np.array([[1., 2., 3.]])
x2 = np.array([[1., 2., 3.], [1., 3., 5]])
print(softmax(x1))
print("---")
print(softmax(x2))

・実行結果

[[0.09003057 0.24472847 0.66524096]]
---
[[0.09003057 0.24472847 0.66524096]
 [0.01587624 0.11731043 0.86681333]]

ソフトマックス関数の演算とクロスエントロピー誤差関数の計算などの処理は下記のように実装することができる。

def cross_entropy_error(y,t):
    delta = 1e-7
    return -np.sum(t * np.log(y+delta))

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, dout=1.):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

上記のSoftmaxWithLossクラスは下記のように使用できる。

x = np.array([[1., 2., 3.]])  # x is 2D-Array
t = np.array([0., 1., 0.])

calc_loss = SoftmaxWithLoss()

# forward
loss = calc_loss.forward(x,t)

# backward
dx = calc_loss.backward()

print(loss)
print(dx)

・実行結果

1.407605555828337
[[ 0.03001019 -0.25175718  0.22174699]]

ニューラルネットワーク

二層のニューラルネットワークを取り扱うにあたって、下記のようにTwoLayerNetクラスを定義する。

from collections import OrderedDict

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        self.params = {}
        self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params["b1"] = np.zeros(hidden_size)
        self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params["b2"] = np.zeros(output_size)

        # generate layers
        self.layers = OrderedDict()
        self.layers["Affine1"] = Affine(self.params["W1"], self.params["b1"])
        self.layers["Relu1"] = Relu()
        self.layers["Affine2"] = Affine(self.params["W2"], self.params["b2"])
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x

    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def calc_gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = self.lastLayer.backward(1.)
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # output
        grads = {}
        grads["W1"] = self.layers["Affine1"].dW
        grads["b1"] = self.layers["Affine1"].db
        grads["W2"] = self.layers["Affine2"].dW
        grads["b2"] = self.layers["Affine2"].db

        return grads

上記のTwoLayerNetを元に下記のようにニューラルネットワークの学習を行える。

np.random.seed(10)

alpha = 0.1
x = np.array([[1., 1., 1.], [1., 3., 5.]])
t = np.array([[1., 0.], [0., 1.]])

network = TwoLayerNet(input_size=3, hidden_size=5, output_size=2)

for i in range(100):
    grad = network.calc_gradient(x, t)

    for key in ("W1", "b1", "W2", "b2"):
        network.params[key] -= alpha * grad[key]

    if (i+1)%20==0:
        print(softmax(network.predict(x)))

・実行結果

[[0.48243772 0.51756228]
 [0.38127236 0.61872764]]
[[0.58016734 0.41983266]
 [0.1216153  0.8783847 ]]
[[0.75082177 0.24917823]
 [0.05060992 0.94939008]]
[[0.81603073 0.18396927]
 [0.01974114 0.98025886]]
[[0.85941286 0.14058714]
 [0.01267276 0.98732724]]

自動微分・誤差逆伝播の理論と応用①:自動微分の仕組みと基本的な実装方針

自動微分(Automatic Differentiation)は大規模なニューラルネットワークであるDeepLearningの学習における誤差逆伝播などに用いられる手法です。当記事では自動微分の仕組みとPythonを用いた基本的な実装方針について取りまとめました。
作成にあたっては「ゼロから作るDeep Learning」の第$5$章「誤差逆伝播法」の内容を主に参照しました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

自動微分の仕組み

合成関数の微分

$y=f(u), u = g(x)$のとき合成関数$y=f(g(x))$の$x$に関する微分は下記のように得られる。

$$
\large
\begin{align}
\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}
\end{align}
$$

上記の「合成関数の微分の公式」は「連鎖率(chain rule)」のようにいう場合もある。式の詳しい導出は下記で取り扱った。

数式微分・数値微分の活用における課題

計算機で微分を取り扱う際の手法は数式微分(Symbolic differenetiation)、数値微分(Numerical differenetiation)、自動微分(Automatic differenetiation)の$3$つに大別できる。

「数式微分」は数学的に導関数を計算し入力値を代入することで計算を行う手法、「数値微分」は$\displaystyle \frac{f(x+\Delta)-f(x)}{\Delta x}$に基づいて近似的に計算を行う手法である。ニューラルネットワークの誤差関数のような複雑な合成関数の微分を取り扱う場合、数式微分では導関数の導出が大変複雑になり、数値微分はパラメータ数が多い場合計算量が多くなる。このような解決にあたって用いられるのが自動微分である。

たとえば下記の演習ではロジスティック回帰の勾配の計算について取り扱ったが、途中式がなかなか複雑である。ニューラルネットワークはロジスティック回帰を複雑化したものであると見なすこともできるので、数式微分を用いると計算がさらに複雑になる。

自動微分の仕組み

前項の課題の解決にあたってニューラルネットワークの学習にあたって実用的に用いられるのが自動微分(Automatic Differentiation)である。自動微分は数式微分と同様に「合成関数の微分の公式」を用いるが、「中間変数の値を代入してから積を計算すること」によりスムーズに計算を行うことができる。

たとえば$f(x) = e^{x^{2}}$の$f'(1)$の値を計算するにあたって、数式微分と自動微分はそれぞれ下記のように値を計算する。

・数式微分
$u=x^{2}, y=e^{u}$とおき、下記のように$f'(x)$を計算する。
$$
\large
\begin{align}
f'(x) &= \frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} \\
&= e^{u} \cdot 2x \\
&= 2x e^{x^{2}}
\end{align}
$$

上記より$f'(1) = 2e^{1} = 2e$の値を計算する。

・自動微分
$u=x^{2}, y=e^{u}$とおき、下記のように$f'(x)$を計算する。
$$
\large
\begin{align}
f'(x) &= \frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} \\
&= e^{u} \cdot 2x
\end{align}
$$

$x=1$のとき$u=1$であるので、$f'(1) = e^{1} \cdot 2 = 2e$を計算する。

「数式微分」と「自動微分」は基本的には同じ公式に基づいて同様な計算を行うが、「数式微分」が「すべて$x$の式で表さなければいけない」一方で「自動微分」は「中間変数の$u$が得られれば計算が可能」である。

よって、中間変数の$u$の値を効率よく保持しておけるならば「自動微分」に基づいてシンプルに結果を計算することができる。

自動微分の基本的な実装方針

実装方針

基本的には「演算」をクラス化し、順伝播(forward propagation)と逆伝播(back propagation)を同じクラスで取り扱うことで逆伝播の計算の効率化を実現する。

乗算のクラス化

全容

乗算は下記のようにクラス化することができる。

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

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

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x
        return dx, dy

上記のクラスは下記のように用いることができる。

apple = 100.
apple_num = 2.
tax = 1.1

mul_apple_layer = MulLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

print(price)

# backward
dprice = 1.
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print(dapple_price, dtax)
print(dapple, dapple_num)

・実行結果

220.00000000000003
1.1 200.0
2.2 110.00000000000001

順伝播による計算

xyの積の計算は下記のようにforwardメソッドで実装される。

class MulLayer:
    〜省略〜

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

    〜省略〜

out = x*youtを計算しreturnで出力するのと同時にself.x = xself.y = yによって入力変数をインスタンスに保持させることを抑えておくと良い。

逆伝播による勾配の取得

xyの積の計算は下記のようにforwardメソッドで実装される。

class MulLayer:
    〜省略〜

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x
        return dx, dy

xyを$x$について偏微分すると$y$、$y$について偏微分すると$x$、が得られることからdx = dout * self.ydy = dout * self.xの処理を理解すると良い。

加算のクラス化

全容

class AddLayer:
    def __init__(self):
        pass

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

    def backward(self, dout):
        dx = dout*1
        dy = dout*1
        return dx, dy

上記のクラスは下記のように用いることができる。

apple = 100.
apple_num = 2.
orange = 150.
orange_num = 3.
tax = 1.1

mul_apple_layer = MulLayer()
mul_orange_layer = MulLayer()
add_apple_orange_layer = AddLayer()
mul_tax_layer = MulLayer()

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
orange_price = mul_orange_layer.forward(orange, orange_num)
all_price = add_apple_orange_layer.forward(apple_price, orange_price)
price = mul_tax_layer.forward(all_price, tax)

print(all_price)
print(price)
print("===")

# backward
dprice = 1.
dall_price, dtax = mul_tax_layer.backward(dprice)
dapple_price, dorange_price = add_apple_orange_layer.backward(dall_price)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)
dorange, dorange_num = mul_orange_layer.backward(dorange_price)

print(dall_price, dtax)
print(dapple, dapple_num)
print(dorange, dorange_num)

・実行結果

650.0
715.0000000000001
===
1.1 650.0
2.2 110.00000000000001
3.3000000000000003 165.0

順伝播による計算

class AddLayer:
    〜省略〜

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

    〜省略〜

$x+y$を$x$について偏微分すると$1$、$y$について偏微分すると$1$が得られることから$x$と$y$の値を保持する必要がないことに着目しておくと良い。このように中間変数の値を保持するかどうかは逆伝播の際に用いるかどうかの観点から確認しておくと良い。

逆伝播による勾配の取得

class AddLayer:
    〜省略〜

    def backward(self, dout):
        dx = dout*1
        dy = dout*1
        return dx, dy

活性化関数のクラス化

ReLU

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

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

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

上記のクラスは下記のように使用することができる。

import numpy as np

calc_Relu = Relu()

# forward
x = np.arange(-1., 1., 0.1)
y = calc_Relu.forward(x)

print(y)

# backward
dout = np.ones(20)
dx = calc_Relu.backward(dout)

print(dx)

・実行結果

[0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7
 0.8 0.9]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 1. 1. 1. 1. 1. 1.]

Sigmoid関数

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

    def forward(self, x):
        out = 1./(1.+np.exp(-x))
        self.out = out
        return out

    def backward(self, dout):
        dx = dout * (1.-self.out) * self.out
        return dx

上記のクラスは下記のように使用することができる。

import numpy as np

calc_Sigmoid = Sigmoid()

# forward
x = np.arange(-1., 1., 0.1)
y = calc_Sigmoid.forward(x)

print(y)

# backward
dout = np.ones(20)
dx = calc_Sigmoid.backward(dout)

print(dx)

・実行結果

[0.26894142 0.2890505  0.31002552 0.33181223 0.35434369 0.37754067
 0.40131234 0.42555748 0.450166   0.47502081 0.5        0.52497919
 0.549834   0.57444252 0.59868766 0.62245933 0.64565631 0.66818777
 0.68997448 0.7109495 ]
[0.19661193 0.20550031 0.2139097  0.22171287 0.22878424 0.23500371
 0.24026075 0.24445831 0.24751657 0.24937604 0.25       0.24937604
 0.24751657 0.24445831 0.24026075 0.23500371 0.22878424 0.22171287
 0.2139097  0.20550031]

計算例:ロジスティック回帰のパラメータ推定

以下、ロジスティック回帰のパラメータ推定を題材に自動微分の実装について確認を行う。まず、下記に基づいてサンプリングを行う。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from scipy import stats

sns.set_theme()

np.random.seed(1)

n = 500
t_a = 1.2 
t_b = 0.5

x = np.random.rand(n)*10.-5.
t_p = 1./(1.+np.e**(-(t_a*x+t_b)))
t = stats.binom.rvs(size=x.shape[0],p=t_p,n=1)

x_ = np.arange(-5.0, 5.01, 0.01)

plt.scatter(x,t,color="lightgreen")
plt.plot(x_, 1./(1.+np.e**(-(t_a*x_+t_b))), color="green")
plt.show()

・実行結果

上記に対し、下記を実行することで自動微分と勾配法に基づいてパラメータ推定を行うことができる。

alpha = 0.01
a, b = 0.5, 0.

mul_coef_layer = MulLayer()
add_bias_layer = AddLayer()
calc_Sigmoid = Sigmoid()

for i in range(100):
    # forward
    u1 = mul_coef_layer.forward(x, a)
    u2 = add_bias_layer.forward(u1, b)
    y = calc_Sigmoid.forward(u2)
    #loss = t*np.log(y) + (1-t)*np.log(1-y)

    # backward
    #dloss = 1.
    dy = t/y - (1-t)/(1-y)
    du2 = calc_Sigmoid.backward(dy)
    du1, db = add_bias_layer.backward(du2)
    dx, da = mul_coef_layer.backward(du1)
    
    a += alpha*np.sum(da)
    b += alpha*np.sum(db)
    if (i+1)%10==0:
        print(a,b)

・実行結果

1.110878642205875 0.6047959437112932
1.1110570172855048 0.6055201115906093
1.1110586396377382 0.6055267041766271
1.1110586544122127 0.6055267642136816
1.1110586545467607 0.6055267647604263
1.1110586545479861 0.6055267647654055
1.111058654547997 0.6055267647654506
1.1110586545479972 0.6055267647654511
1.1110586545479972 0.6055267647654511
1.1110586545479972 0.6055267647654511

サンプリングに用いたパラメータの値が$a=1.2, b=0.5$であるので、上記は概ね適切な結果であることが確認できる。

注意:当記事ではクロスエントロピー誤差関数の実装を行なっていないので、dy = t/y - (1-t)/(1-y)は出力層の微分の式を用いた。

微分方程式(differential equation)の基本事項まとめ⑤|線形微分方程式

微分方程式(differential equation)は多くの応用先がありますが、統計学を学ぶにあたってもハザード関数から確率密度関数を導出する際などに用いられます。当記事では線形微分方程式の基本的な解法について概要と計算例を取りまとめました。
作成にあたっては「チャート式シリーズ 大学教養 微分積分」の第$9$章「微分方程式」を主に参考にしました。

・数学まとめ
https://www.hello-statisticians.com/math_basic

線形微分方程式

線形微分方程式の定義

$x$の関数$q(x), p_0(x), \cdots , p_{n-1}(x)$を用いて下記のような形式で表される微分方程式を未知関数$y=y(x)$に関する$n$階の線形微分方程式という。
$$
\large
\begin{align}
y^{(n)} + p_{n-1}(x) y^{(n-1)} + \cdots + p_1(x) y’ + p_0(x) y + q(x) = 0 \quad (1.1)
\end{align}
$$

また、$(1.1)$式の線形微分方程式について$q(x)=0$が成立する場合「同次」といい、$q(x) \neq 0$のとき「非同次」であるという。

微分作用素

$$
\large
\begin{align}
D = \frac{d}{dx}
\end{align}
$$

線形微分方程式は上記のような微分作用素$D$を用いると見やすく表すことができる。

定数変化法

同次の微分方程式を解いたのちに解の定数$C$を$C(x)$に置き換えて非同次の微分方程式に代入し、特殊解を求める方法を定数変化法という。

多項式の分解と微分方程式

定数係数同次線形微分方程式の解

同次形の微分方程式の計算例

以下、「チャート式シリーズ 大学教養 微分積分」の例題の確認を行う。

基本例題$168$

・$[1]$
$$
\large
\begin{align}
y’ + \frac{1}{x}y – x^2 = 0 \quad (2.1.1)
\end{align}
$$

以下、定数変化法を用いて解く。同次形の微分方程式$\displaystyle y’ + \frac{1}{x}y = 0$の解は下記のように得られる。
$$
\large
\begin{align}
y’ + \frac{1}{x}y &= 0 \\
\frac{dy}{dx} &= -\frac{y}{x} \\
\frac{dy}{y} &= -\frac{dx}{x} \\
\log{|y|} &= -\log{|x|} + C \\
\log{|xy|} &= C \\
y &= \frac{c_1}{x} \quad (2.1.2)
\end{align}
$$

上記に対し$c_1=p(x)$とおき、$(2.1.1)$式に代入すると下記が得られる。
$$
\large
\begin{align}
\left( \frac{p(x)}{x} \right)’ + \frac{p(x)}{x^2} – x^2 &= 0 \\
\frac{p'(x)x – \cancel{p(x)}}{x^2} + \cancel{\frac{p(x)}{x^2}} – x^2 &= 0 \\
\frac{p'(x)}{x} – x^2 &= 0 \\
\frac{dp}{dx} \cdot \frac{1}{x} &= x^{2} \\
dp &= x^{3} dx \\
p &= \frac{1}{4}x^{4} + C
\end{align}
$$

上記に基づいて$\displaystyle c_1 = p(x) = \frac{1}{4}x^{4}+C$を$(2.1.2)$式に代入すると下記が得られる。
$$
\large
\begin{align}
y &= \frac{c_1}{x} \\
&= \frac{1}{4}x^{3} + \frac{C}{4}
\end{align}
$$

・$[2]$
$$
\large
\begin{align}
y’ + y \cos{x} – \sin{x} \cos{x} = 0 \quad (2.2.1)
\end{align}
$$

以下、定数変化法を用いて解く。同次形の微分方程式$\displaystyle y’ + y \cos{x} = 0$の解は下記のように得られる。
$$
\large
\begin{align}
y’ + y \cos{x} &= 0 \\
\frac{dy}{dx} &= -y \cos{x} \\
\frac{dy}{y} &= – \cos{x} dx \\
\log{|y|} &= -\sin{x} + C \\
|y| &= e^{-\sin{x}+C} \\
y &= c_1 e^{-\sin{x}} \quad (2.2.2)
\end{align}
$$

上記に対し$c_1=p(x)$とおき、$(2.2.1)$式に代入すると下記が得られる。
$$
\large
\begin{align}
y’ + y \cos{x} – \sin{x} \cos{x} &= 0 \quad (2.2.1) \\
\left( p(x) e^{-\sin{x}} \right)’ + p(x) e^{-\sin{x}} \cos{x} &= \sin{x} \cos{x} \\
p'(x) e^{-\sin{x}} + \cancel{p(x) e^{-\sin{x}} \cdot (-\cos{x})} + \cancel{p(x) e^{-\sin{x}} \cos{x}} &= \sin{x} \cos{x} \\
\frac{dp}{dx} e^{-\sin{x}} &= \sin{x} \cos{x} \\
\int dp &= \int \sin{x} \cos{x} e^{\sin{x}} dx \quad (2.2.3)
\end{align}
$$

上記に対し、$t = \sin{x}$とおくと下記が成立する。
$$
\large
\begin{align}
\frac{dt}{dx} &= \cos{x} \\
\cos{dx} dx &= dt
\end{align}
$$

よって$(2.2.3)$式は下記のように変形できる。
$$
\large
\begin{align}
\int dp &= \int \sin{x} \cos{x} e^{\sin{x}} dx \quad (2.2.3) \\
\int dp &= \int t e^{t} dt \\
p &= t e^{t} – \int (t)’ e^{t} dt \\
p &= t e^{t} – \int e^{t} dt \\
&= t e^{t} – e^{t} + C \\
&= e^{\sin{x}}(\sin{x} – 1) + C
\end{align}
$$

上記に基づいて$\displaystyle c_1 = p(x) = e^{\sin{x}}(\sin{x} – 1) + C$を$(2.2.2)$式に代入することで下記が得られる。
$$
\large
\begin{align}
y &= e^{-\sin{x}} (e^{\sin{x}}(\sin{x} – 1) + C) \\
&= \sin{x} + C e^{-\sin{x}} – 1
\end{align}
$$

・$[3]$
$$
\large
\begin{align}
y’ + 2y – 3 e^{4x} = 0 \quad (2.3.1)
\end{align}
$$

以下、定数変化法を用いて解く。同次形の微分方程式$\displaystyle y’ + 2y = 0$の解は下記のように得られる。
$$
\large
\begin{align}
y’ + 2y &= 0 \\
\frac{dy}{dx} &= -2y \\
\frac{dy}{y} &= -2 dx \\
\log{|y|} &= -2x + C \\
|y| &= e^{-2x+C} \\
y &= \pm e^{C} e^{-2x} \\
y &= c_1 e^{-2x} \quad (2.3.2)
\end{align}
$$

上記に対し$c_1=p(x)$とおき、$(2.3.1)$式に代入すると下記が得られる。
$$
\large
\begin{align}
y’ + 2y – 3 e^{4x} &= 0 \quad (2.3.1) \\
\left( p(x) e^{-2x} \right)’ + 2p(x) e^{-2x} &= 3 e^{4x} \\
p'(x)e^{-2x} – \cancel{2p(x) e^{-2x}} + \cancel{2p(x) e^{-2x}} &= 3 e^{4x} \\
\frac{dp}{dx} &= 3e^{6x} \\
\int dp &= \int 3e^{6x} dx \\
p &= \frac{1}{2} e^{6x} + C
\end{align}
$$

上記に基づいて$\displaystyle c_1 = p(x) = \frac{1}{2} e^{6x} + C$を$(2.3.2)$式に代入すると下記が得られる。
$$
\large
\begin{align}
y &= c_1 e^{-2x} \\
&= \left( \frac{1}{2} e^{6x} + C \right) e^{-2x} \\
&= \frac{1}{2} e^{4x} + C e^{-2x}
\end{align}
$$

基本例題$169$

基本例題$171$

基本例題$172$

基本例題$173$

重要例題$103$

$$
\large
\begin{align}
y’ + y \tan{x} = \frac{1}{\cos{x}}
\end{align}
$$

以下、定数変化法を用いて解く。同次形の微分方程式$y’ + y \tan{x} = 0$の解は下記のように得られる。

$[1] \,$ $y=0$のとき
定数関数$y=0$は$y’ + y \tan{x} = 0$の解である。

$[2] \,$ $y \neq 0$のとき
$y’ + y \tan{x} = 0$は下記のように変形できる。
$$
\large
\begin{align}
y’ + y \tan{x} &= 0 \\
\frac{dy}{dx} &= -y \tan{x} \\
\frac{dy}{y} &= -\tan{x} dx \\
\int \frac{1}{y} dy &= \int \frac{(\cos{x})’}{\cos{x}} dx \\
\log{|y|} &= \log{|\cos{x}|} + C \\
|y| &= e^{C} |\cos{x}| \\
y &= \pm e^{C} \cos{x} \\
&= c_1 \cos{x}
\end{align}
$$

$c_1=0$のとき$y=0$であるから、$[1]$の結果も$y=c_1 \cos{x}$で表すことができる。ここで$c_1=p(x)$とおき、元の式に代入すると下記が得られる。
$$
\large
\begin{align}
y’ + y \tan{x} &= \frac{1}{\cos{x}} \\
\left( p(x) \cos{x} \right)’ + p(x) \tan{x} &= \frac{1}{\cos{x}} \\
p'(x) \cos{x} – p(x) \sin{x} + p(x) \cancel{\cos{x}} \cdot \frac{\sin{x}}{\cancel{\cos{x}}} &= \frac{1}{\cos{x}} \\
p'(x) \cos{x} – \cancel{p(x) \sin{x}} + \cancel{p(x) \sin{x}} &= \frac{1}{\cos{x}} \\
\frac{dp}{dx} \cos{x} &= \frac{1}{\cos{x}} \\
dp &= \frac{1}{\cos^{2}{x}} dx \\
p &= \tan{x} + C
\end{align}
$$

上記に基づいて$\displaystyle c_1 = p(x) = \tan{x} + C$を$y = c_1 \cos{x}$式に代入すると下記が得られる。
$$
\large
\begin{align}
y &= c_1 \cos{x} \\
&= (\tan{x} + C) \cos{x} \\
&= \sin{x} + C \cos{x}
\end{align}
$$

微分方程式(differential equation)の基本事項まとめ④|完全微分形と積分因子

微分方程式(differential equation)は多くの応用先がありますが、統計学を学ぶにあたってもハザード関数から確率密度関数を導出する際などに用いられます。当記事では完全微分形の微分方程式の解法や積分因子を用いた完全微分形への式変形について取り扱いました。
作成にあたっては「チャート式シリーズ 大学教養 微分積分」の第$9$章「微分方程式」を主に参考にしました。

・数学まとめ
https://www.hello-statisticians.com/math_basic

完全微分形と積分因子

完全微分形

関数$F(x,y)$について下記のように偏微分$P(x,y)$と$Q(x,y)$を定義する。
$$
\large
\begin{align}
P(x,y) &= F_{x}(x,y) = \frac{\partial F(x,y)}{\partial x} \\
Q(x,y) &= F_{y}(x,y) = \frac{\partial F(x,y)}{\partial y}
\end{align}
$$

このとき下記のような微分方程式を完全微分形という。
$$
\large
\begin{align}
P(x,y)dx + Q(x,y)dy = 0 \quad (1.1)
\end{align}
$$

逆に$(1.1)$式のような微分方程式が完全微分形であることは、下記が成立するかどうかを調べれば良い。
$$
\large
\begin{align}
\frac{\partial P(x,y)}{\partial y} = P_{y}(x,y) = Q_{x}(x,y) = \frac{\partial Q(x,y)}{\partial x} \quad (1.2)
\end{align}
$$

ここまでの内容は「完全微分形 $\, \iff \,$ $(1.2)$式が成立」のようにまとめられる。また、このとき$F(x,y)$は下記のような計算により得ることができる。
$$
\large
\begin{align}
F(x,y) = \int_{a}^{x} P(u,b) du + \int_{b}^{y} Q(x,v) dv
\end{align}
$$

上記の$(a,b)$は$P(x,y), Q(x,y)$の定義域内の任意の点を用いて良いので、$a=b=0$をなるべく用いると計算がしやすい。

積分因子

完全微分形とは限らない微分方程式$Pdx+Qdy=0$に対し、ある関数$\mu(x,y)$を両辺に掛けることで$\mu P dx + \mu Q dy = 0$が完全微分形になることがある。このような$\mu(x,y)$を積分因子という。

積分因子を得るのは一般にそれほど簡単ではないが、以下のような場合は複雑な計算を行わずに積分因子を得ることができる。

$[1] \,$ $\displaystyle R(x)=\frac{1}{Q}(P_y-Q_x)$が$x$のみの関数であるとき、$\displaystyle \mu = \exp{\left( \int R(x) dx \right)}$とおくと$\mu=\mu(x)$は積分因子である。
$[2] \,$ $\displaystyle S(x)=-\frac{1}{P}(P_y-Q_x)$が$y$のみの関数であるとき、$\displaystyle \mu = \exp{\left( \int S(x) dx \right)}$とおくと$\mu=\mu(y)$は積分因子である。

同次形の微分方程式の使用例

以下、「チャート式シリーズ 大学教養 微分積分」の例題の確認を行う。

基本例題$165$

・$[1]$
$$
\large
\begin{align}
(2x+4y+1)dx + (4x+3y-1)dy = 0 \quad (2.1.1)
\end{align}
$$

上記について$P(x,y)=2x+4y+1, \, Q(x,y)=4x+3y-1$のようにおくと、$P_{y}(x,y)=4=Q_{x}(x,y)$より、$(2.1.1)$式は完全微分形である。よって、下記の計算に基づいて$F(x,y)$を定義する。
$$
\large
\begin{align}
F(x,y) &= \int_{0}^{x} P(u,0) du + \int_{0}^{y} Q(x,v) dv \\
&= \int_{0}^{x} (2u+1) du + \int_{0}^{y} (4x+3v-1) dv \\
&= \left[ u^{2}+u \right]_{0}^{x} + \left[ 4xv + \frac{3}{2}v^{2}-v \right]_{0}^{y} \\
&= x^{2} + x + 4xy + \frac{3}{2}y^{2} – y \\
&= x^{2} + 4xy + \frac{3}{2}y^{2} + x – y
\end{align}
$$

ここで$F(x,y) = x^{2}+4xy+\frac{3}{2}y^{2}+x-y$について下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= 2x + 4y + 1 = P(x,y) \\
F_{y}(x,y) &= 4x + 3y – 1 = Q(x,y)
\end{align}
$$

よって微分方程式$(2.1.1)$の解は$\displaystyle x^{2}+4xy+\frac{3}{2}y^{2}+x-y = C$である。

・$[2]$

・$[3]$

基本例題$166$

・$[1]$
$$
\large
\begin{align}
(x+y^{2}+1)dx + 2ydy = 0 \quad (2.2.1)
\end{align}
$$

$P(x,y)=x+y^{2}+1, \, Q(x,y)=2y$とおく。このとき$P_{y}(x,y)=2y, \, Q_{x}(x,y)=0$より下記が成立する。
$$
\large
\begin{align}
\frac{P_{y}(x,y)-Q_{x}(x,y)}{Q(x,y)} = \frac{2y-0}{2y} = 1
\end{align}
$$

ここで$\displaystyle \mu(x)=\exp{ \left( \int 1 dx \right) }$に基づいて$\mu(x)=e^{x}, \, \mu(x)P(x,y)=e^{x}(x+y^{2}+1), \, \mu(x)Q(x,y)=2e^{x}y^{2}$を定義すると下記が成立する。
$$
\large
\begin{align}
(\mu(x)P(x,y))_{y} = 2 e^{x} y = (\mu(x)Q(x,y))_{x}
\end{align}
$$

よって$e^{x}(x+y^{2}+1)dx + 2e^{x}ydy = 0$が完全微分形である。また、$F(x,y)=e^{x}(x+y^{2})$とおくと下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= e^{x}(x+y^{2}+1) = \mu(x)P(x,y) \\
F_{y}(x,y) &= 2e^{x}y = \mu(x)Q(x,y)
\end{align}
$$

よって微分方程式$(2.1.1)$の解は$\displaystyle e^{x}(x+y^{2}) = C$である。

・$[2]$
$$
\large
\begin{align}
(1-xy)dx + (xy-x^{2})dy = 0 \quad (2.2.2)
\end{align}
$$

$P(x,y)=1-xy, \, Q(x,y)=xy-x^{2}$とおく。このとき$P_{y}(x,y)=-x, \, Q_{x}(x,y)=y-2x$より下記が成立する。
$$
\large
\begin{align}
\frac{P_{y}(x,y)-Q_{x}(x,y)}{Q(x,y)} &= \frac{-x-(y-2x)}{xy-x^{2}} \\
&= \frac{-(y-x)}{x(y-x)} \\
&= \frac{1}{x}
\end{align}
$$

ここで下記のように$\mu(x)$を定義する。
$$
\large
\begin{align}
\mu(x) &= \exp{ \left( \int -\frac{1}{x} dx \right) } \\
&= e^{-\log{|x|}} \\
&= |x|^{-1} = \frac{1}{|x|}
\end{align}
$$

以下、$x>0$のときと$x<0$のときで場合分けし、確認を行う。

$a) \, x>0$のとき
$\mu(x)P(x,y)$、$\mu(x)Q(x,y)$は下記のように表される。
$$
\large
\begin{align}
\mu(x)P(x,y) &= \frac{1}{x}(1-xy) \\
\mu(x)Q(x,y) &= \frac{x(y-x)}{x} = y-x
\end{align}
$$

上記に対し、下記が成立する。
$$
\large
\begin{align}
(\mu(x)P(x,y)){y} &= \frac{1}{x} \cdot (-x) = -1 \\
(\mu(x)Q(x,y)){x} &= -1
\end{align}
$$

よって$\mu(x)(1-xy)dx + \mu(x)(xy-x^{2})dy = 0$は完全微分形である。ここで$F(x,y)$を下記のように定義する。
$$
\large
\begin{align}
F(x,y) &= \int_{1}^{x} \mu(u) P(u,0) du + \int_{0}^{y} Q(x,v) dv \\
&= \int_{1}^{x} \frac{1}{u} du + \int_{0}^{y} (v-x) dv \\
&= \left[ \log{|u|} \right]_{1}^{x} + \left[ \frac{1}{2}v^{2} – xv \right]_{0}^{y} \\
&= \log{x} – xy + \frac{1}{2}y^{2}
\end{align}
$$

このとき下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= \frac{1}{x} – y = \frac{1}{x}(1-xy) = \mu(x)P(x,y) \\
F_{y}(x,y) &= -x + y = y-x = \mu(x)Q(x,y)
\end{align}
$$

よって$x>0$のとき微分方程式$(2.2.2)$の解は$\displaystyle \log{x} – xy + \frac{1}{2}y^{2} = C_1$である。

$b) \, x<0$のとき
$a)$と同様な計算により$\displaystyle -\log{(-x)} + xy – \frac{1}{2}y^{2} = C_2$が得られる。

$a)$、$b)$より、$(2.2.2)$の解は$\displaystyle \log{|x|} – xy + \frac{1}{2}y^{2} = C$である。

・$[3]$

基本例題$167$

・$[1]$
$$
\large
\begin{align}
(3x^{2}y – 6x)dx + (x^{3}+2y)dy = 0 \quad (2.3.1)
\end{align}
$$

上記について$P(x,y)=3x^{2}y – 6x, \, Q(x,y)=x^{3}+2y$のようにおくと、$P_{y}(x,y)=3x^{2}=Q_{x}(x,y)$より、$(2.3.1)$式は完全微分形である。よって、下記の計算に基づいて$F(x,y)$を定義する。
$$
\large
\begin{align}
F(x,y) &= \int_{0}^{x} P(u,0) du + \int_{0}^{y} Q(x,v) dv \\
&= \int_{0}^{x} (-6u) du + \int_{0}^{y} (x^{3}+2v) dv \\
&= \left[ -3u^{2} \right]_{0}^{x} + \left[ x^{3}v + v^{2} \right]_{0}^{y} \\
&= -3x^{2} + x^{3}y + y^{2} \\
&= x^{3}y – 3x^{2} + y^{2}
\end{align}
$$

ここで$F(x,y) = x^{3}y – 3x^{2} + y^{2}$について下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= 3x^{2}y – 6x = P(x,y) \\
F_{y}(x,y) &= x^{3} + 2y = Q(x,y)
\end{align}
$$

よって微分方程式$(2.3.1)$の解は$\displaystyle x^{3}y – 3x^{2} + y^{2} = C$である。

・$[2]$
$$
\large
\begin{align}
(x^{3} + 2xy + y)dx + (y^{3} + x^{2} + x)dy = 0 \quad (2.3.2)
\end{align}
$$

上記について$P(x,y)=x^{3} + 2xy + y, \, Q(x,y)=y^{3} + x^{2} + x$のようにおくと、$P_{y}(x,y)=2x+1=Q_{x}(x,y)$より、$(2.3.2)$式は完全微分形である。よって、下記の計算に基づいて$F(x,y)$を定義する。
$$
\large
\begin{align}
F(x,y) &= \int_{0}^{x} P(u,0) du + \int_{0}^{y} Q(x,v) dv \\
&= \int_{0}^{x} u^{3} du + \int_{0}^{y} (v^{3} + x^{2} + x) dv \\
&= \left[ \frac{1}{4}u^{4} \right]_{0}^{x} + \left[ \frac{1}{4}v^{4} + x^{2}v + xv \right]_{0}^{y} \\
&= \frac{1}{4}x^{4} + \frac{1}{4}y^{4} + x^{2}y + xy \\
&= \frac{1}{4}x^{4} + x^{2}y + xy + \frac{1}{4}y^{4}
\end{align}
$$

ここで$\displaystyle F(x,y) = \frac{1}{4}x^{4} + x^{2}y + xy + \frac{1}{4}y^{4}$について下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= x^{3} + 2xy + y = P(x,y) \\
F_{y}(x,y) &= y^{3} + x^{2} + x = Q(x,y)
\end{align}
$$

よって微分方程式$(2.3.2)$の解は$\displaystyle \frac{1}{4}x^{4} + x^{2}y + xy + \frac{1}{4}y^{4} = C$である。

・$[3]$
$$
\large
\begin{align}
(\cos{y} + y \cos{x})dx + (\sin{x} – x \sin{y})dy = 0 \quad (2.3.3)
\end{align}
$$

上記について$P(x,y)=\cos{y} + y \cos{x}, \, Q(x,y)=\sin{x} – x \sin{y}$のようにおくと、$P_{y}(x,y)=\cos{x}-\sin{y}=Q_{x}(x,y)$より、$(2.3.3)$式は完全微分形である。よって、下記の計算に基づいて$F(x,y)$を定義する。
$$
\large
\begin{align}
F(x,y) &= \int_{0}^{x} P(u,0) du + \int_{0}^{y} Q(x,v) dv \\
&= \int_{0}^{x} 1 du + \int_{0}^{y} (\sin{x} – x \sin{v}) dv \\
&= \left[ u \right]_{0}^{x} + \left[ v \sin{x} + x \cos{v} \right]_{0}^{y} \\
&= x + y \sin{x} + x \cos{y} – x \\
&= x \cos{y} + y \sin{x}
\end{align}
$$

ここで$\displaystyle F(x,y) = x \cos{y} + y \sin{x}$について下記が成立する。
$$
\large
\begin{align}
F_{x}(x,y) &= \cos{y} + y \cos{x} = P(x,y) \\
F_{y}(x,y) &= -x \sin{y} + \sin{x} = Q(x,y)
\end{align}
$$

よって微分方程式$(2.3.3)$の解は$\displaystyle x \cos{y} + y \sin{x} = C$である。

・$[4]$

微分方程式(differential equation)の基本事項まとめ②|初期条件・初期値問題

微分方程式(differential equation)は多くの応用先がありますが、統計学を学ぶにあたってもハザード関数から確率密度関数を導出する際などに用いられます。当記事では微分方程式を解く際に重要になる「初期条件・初期値問題」について取り扱いました。
作成にあたっては「チャート式シリーズ 大学教養 微分積分」の第$9$章「微分方程式」を主に参考にしました。

・数学まとめ
https://www.hello-statisticians.com/math_basic

初期条件・初期値問題

微分方程式の初期条件で与えられた値を初期値といい、初期値を代入して微分方程式の定数の具体的な値を計算する。このような問題を初期値問題という。

微分方程式における初期値問題の計算例

以下、「チャート式シリーズ 大学教養 微分積分」の例題の確認を行う。

基本例題$162$

・$[1]$
$$
\large
\begin{align}
y’ = xy^2
\end{align}
$$

上記の解の$\displaystyle y = -\frac{2}{x^2 + C}$に$x=1, y=-1$を代入すると下記が得られる。
$$
\large
\begin{align}
y &= -\frac{2}{x^2 + C} \\
-1 &= -\frac{2}{1^2 + C} \\
-1-C &= -2 \\
C &= -1+2 \\
&= 1
\end{align}
$$

よって$\displaystyle y = -\frac{2}{x^2 + 1}$が得られる。

・$[2]$
$$
\large
\begin{align}
xy’ + y = y^2
\end{align}
$$

上記の解の$\displaystyle y = \frac{1}{Cx+1}$に$\displaystyle x=1, y=\frac{3}{2}$を代入すると下記が得られる。
$$
\large
\begin{align}
y &= \frac{1}{Cx+1} \\
\frac{3}{2} &= \frac{1}{C+1} \\
3(1+C) &= 2 \\
3C &= -1 \\
C &= -\frac{1}{3}
\end{align}
$$

よって$\displaystyle y = \frac{1}{-x/3+1} = \frac{3}{3-x}$が得られる。

・$[3]$
$$
\large
\begin{align}
y’ = e^{x+y}
\end{align}
$$

上記の解の$y = -\log{(-(e^{x} + C))}$に$x=0, y=0$を代入すると下記が得られる。
$$
\large
\begin{align}
y &= -\log{(-(e^{x} + C))} \\
0 &= -\log{(-(e^{0} + C))} \\
\log{(-(1 + C))} &= 0 \\
-(1 + C) &= e^{0} \\
C &= – 1 – 1 \\
&= -2
\end{align}
$$

よって$\displaystyle y = -\log{(2-e^{x})}$が得られる。

基本例題$170$

【数式なし版】直感的に理解するTransformerの仕組みと処理の流れの概要

昨今のDeepLearningの研究を席巻するTransformerの解説は数式を用いたものが多く、なかなか理解が難しいかもしれません。そこで当記事では別途作成を行ったTransformerの解説コンテンツを元に数式を用いないでTransformerの仕組みと処理の流れの概要の取りまとめを行いました。

Introduction

Transformerの概要

TransformerはChatGPT・GPT-$4$など多くのLLMで用いられる主要な仕組みです。元々は$2017$年に機械翻訳の分野で考案された仕組みである一方で、その表現力の高さから現在では文書要約や対話などより難しいタスクへの適用が行われています。

Transformerの処理の全体図:Transformer論文より

Transformerの応用分野

Transformerは大変強力な処理の仕組みであり、様々な応用先があります。NLP分野のみならず画像認識(ViT; Vision Transformerなど)や強化学習、時系列処理などでも用いられることがあります。

より詳しい理解にあたって

当記事では大まかな理解を重視しましたので詳細の解説は行いませんでした。より詳しい理解にあたっては下記などを参照ください。

【Udemy版】直感的に理解するTransformer(統計の森作成)
・【pdf版】直感的に理解するTransformerの仕組み(統計の森作成)

前提知識

Word$2$vec

Word$2$vecは単語(token)をベクトルで表したものです。ベクトルはプログラミング言語における配列と同様で、複数の数字を保持した$1$つの箱をイメージすると良いです。なのでWord$2$vecは『一つ一つの単語が複数の数字を持つ』と理解すれば良いです。

たとえば「本屋に行った。統計の教科書を買った。」から主要な単語を抜き出した「本屋、行く、統計、教科書、買う」はそれぞれ下記のようにベクトル表記することが可能です。

上図の緑の枠が「本屋」に対応するベクトルです。上図では「本屋」という単語を表すにあたって、「場所・書籍・商品」という三つの特徴を持つということを$1$で表しました。同様に「講義・動詞」という二つの特徴を持たないことを$0$で表しました。

このようにWord$2$vecでは単語をベクトルで変換することで意味を持たせることが可能です。Word$2$vecを用いる際は基本的に予め学習させて表を作成しておき、「本屋」であれば「$(1,1,1,0,0)$」というように単に辞書を調べるように取得します。

Word$2$vecを用いた単語のベクトル変換:基本的には検索と同様のイメージで理解できる

厳密には「場所・書籍・商品・講義・動詞」のようにWord$2$vecの要素がそれぞれ明確な意味を持つわけではありませんが、同様な解釈は可能なので基本的にはこのような理解で十分だと思います。

ニューラルネットワーク・深層学習

深層学習(DeepLearning)は大規模なニューラルネットワークで、回帰による予測と基本的には同様な演算の仕組みに基づいて計算が行われます。

回帰分析:水色のサンプルを元に赤の直線を得る

上図は回帰分析の図で、たとえば$x$には「部屋の広さ」、$y$には「家賃」などが設定され、「部屋の広さ」から「家賃」を予測できるように直線を得ます。ここで回帰分析とシンプルなニューラルネットワークであるMLP(Multi Layer Perceptron)の対応は下図のように表されます。

回帰分析とMLP(Multi Layer Perceptron)の対応

上図の対応を元にMLPは回帰における計算を複数回繰り返したものであると認識しておくと良いです。中間層(隠れ層)の値を制御するにあたって活性化関数が用いられることも有名なので合わせて抑えておくと良いと思います。また、深層学習(Deep Learning)は大規模なニューラルネットワークのことを指します。

ニューラルネットワークの構造的仮定の活用①:CNN

MLPの入力層と出力層:入力層$\to$中間層の計算に用いるパラメータの数は「入力層の次元$\times$出力層の次元」で計算できるので、上記では$2 \times 3 = 6$個のパラメータが必要

上図のようなMLP(Multi Layer Perceptron)形式のニューラルネットワークは入力する数字の数(次元ということが多いです)が増えると取り扱いが難しくなります。たとえばピクセルサイズが$500 \times 500$のカラー画像の場合、入力層の次元は下記のようになります。
$$
\large
\begin{align}
500 \times 500 \times 3 = 750{,}000
\end{align}
$$

もし中間層の次元が入力層の次元と同じ$750{,}000$である場合、この計算に必要なパラメータの数は$750{,}000$の$2$乗になるので下記のように計算できます。
$$
\large
\begin{align}
750{,}000^{2} = 562{,}500{,}000{,}000
\end{align}
$$

上記は$5{,}600$億に対応し、昨今注目を集めるPaLMの$5{,}400$億と同様な数字になります。このようにMLPをそのまま適用すると$1$層の計算だけで計算量が大変大きくなります。

この解決にあたっては「入力の特徴を活用する」ということが原則用いられます。たとえば画像であれば「近傍のピクセルのRGB値に近いRGB値を持つ」、言語であれば「近傍の単語によって用いられる単語が決まる」などです。

画像の拡大例:線と背景の境界であってもそれぞれのピクセルは似通った色(RGB値)を持つ

上図は画像を拡大したものですが、拡大を行いピクセル単位に着目することで、それぞれのRGB値は近傍の値に概ね近い値を持つことが確認できます。このことを「高い相関を持つ」ということもあります。「相関」は統計学に出てくる「相関係数」と同じ意味です。

画像の場合はこのように「近傍のピクセルと似たようなRGB値を持つ」という特徴を元に畳み込みニューラルネットワーク(CNN)という手法を用いることが多いです。

ニューラルネットワークの構造的仮定②:RNN

画像ではCNNが用いられることが多い一方で、「時系列処理・信号処理・言語処理」などの数字や記号の列を取り扱う場合はリカレントニューラルネットワーク(RNN)という仕組みが従来的にはよく用いられてきました。

RNNの処理概要:中間層(隠れ層)の値を次の中間層に作用させる、基本的には数値を加算すると理解しておけば良い

入門書などではRNNの改良の一つであるLSTMが紹介されることが多いですが、TransformerによるパラダイムシフトによってLSTMはRNNの一種であると括ることが多くなったことも合わせて抑えておくと良いです。

ニューラルネットワークの構造的仮定③:グラフニューラルネットワーク

主なニューラルネットワークの構成の例にはCNNとRNNが挙げられることが多い一方で、近年ではグラフニューラルネットワーク(GNN)が注目を集めることも多くなりました。GNNはRNNの発展形であると見なすこともできるなど、大変強力な手法です。

グラフニューラルネットワークを理解するにあたっては、下図で取り扱った「グラフ」と「隣接行列」の対応を抑えておくと良いです。

グラフと隣接行列:左がグラフ、右が隣接行列に対応。たとえばグラフで$1$と$2$の点が連結していることを隣接行列では$1$行$2$列と$2$行$1$列の成分が$1$であることで表す。

グラフは点と線を用いて物事の関連性を表す考え方です。駅の路線図やフローチャートのように複雑な物事を簡易的に表すにあたってよく用いられます。グラフニューラルネットワークの解説は抽象的になりがちなので以下具体的な例を元に確認します。

緑の枠で囲った領域はそれぞれニューラルネットワークの中間層(隠れ層)に対応する、ここでは箱が$5$つあるので$5$次元の中間層を図示したものであると理解すればよい。左がRNN、右がグラフニューラルネットワークを表す。

上図はWord$2$vecで確認した「本屋、行く、統計、教科書、買う」の「RNN」と「類似度に基づくGNN」の参照構造です。RNNでは入力の順番に基づいて隠れ層の作用が行われる一方で、GNNではそれ以外の参照構造に基づいて隠れ層同士の相互作用の演算を行うことが可能です。

GNNは参照構造をグラフで与えることで計算を行うことができるので、処理の自由度が非常に大きくなります。たとえば時系列順にグラフを与える場合、GNNはRNNに一致します。

このように自由度の高い処理が実現できるグラフニューラルネットワーク(GNN)を元に単語の類似度に基づいてグラフを構築することで処理を定義したのがTransformerです。Transformerについては詳しくは次節で取り扱います。

なぜRNNでは十分なパフォマンスが出なかったのか

Transformerが出てくる以前は「時系列問題・言語処理」などを取り扱うニューラルネットワークにRNNやRNNを改良したLSTMの使用が検討されることが多かった一方で、入力が$20$〜$50$以上になるとうまく取り扱えないという問題がありました。

上記の問題は「入力を$1$つずつ処理し、中間層(隠れ層)を次の入力に作用させる」RNNの構造に起因します。直感的には「伝言ゲーム」や「都市伝説」のように「伝達処理」を$10$回、$20$回と繰り返すうちに最初の方の情報が消失すると解釈すると良いです。

たとえば一回あたりに$90$%の伝達効率だとする場合、伝達処理を$20$回行うと$0.9^{20}$を元に約$12.2$%、$50$回行うと$0.9^{50}$を元に約$0.5$%が得られます。計算の詳細は流しても良いので、「$90$%で伝達できる場合も回数が多くなると情報が消失する」だけ確実に抑えておきましょう。

Transformerの処理の流れ

Attention

Transformerのベースになった主要な処理の$1$つがAttentionです。Attentionは「中間層(隠れ層)の重み付け和」を計算することで「伝言ゲーム」のように情報が消失するRNNの問題の解決を行う手法です。

RNNとAttentionの図解:Attentionでは全ての中間層の重み付け和を元に出力層の計算を行う、実線は重み付け和を表す。

RNNでは入力する系列(記号列・単語列をまとめて系列といいます)に対し順々に処理を行いましたが、Attentionでは重み付け和を計算することで情報の消失を抑えることができます。これにより系列が長い場合も処理が可能になります。

オリジナルのTransformerではデフォルトで$512$までの長さの系列を取り扱うことが可能になるように実装されることも合わせて抑えておくと良いと思います。

self Attention

Attentionを用いる上で用意する必要があるのが「重み付け和」を計算するときの「重み」です。この値はどのように計算するのが良いでしょうか。「外部から与える」というのも可能である一方で、ニューラルネットワークの構成が大変複雑になるなど課題もありました。

この解決にあたって用いられるのがself Attentionという考え方です。self Attentionでは「入力データから中間層(隠れ層)を得る一方で、隠れ層の値に基づいてAttentionの重みの計算」も行います。

self Attentionでは隠れ層のベクトルの値に基づいてAttention計算に用いる重みの計算を行う

「入力に基づいてどのように重みを計算すると良いか」については様々な手法が考案されましたが、その中で「最もシンプルかつ強力な手法」がTransformerで用いられるDot Product Attentionです。

Dot Product Attention

Dot Product Attentionはself Attentionを実現する一つの手法であり、「単語」などの類似度に基づいてAttentionの重みの計算を行います。ここでDot Productは内積を意味し、大まかにはベクトルの類似度を計算することに対応します。

ベクトルを図で理解する場合、「向き」と「大きさ」を持った「矢印」であると見ることができますが、「内積」はこの「矢印」の類似度を計算する方法です。

$3$つのベクトルの例

たとえば上図のような$3$つのベクトルがある場合、「左と真ん中」は直交するので類似度は$0$、「左と右」、「真ん中と右」は少なからず同じ向きを向くので「$0$より大きい類似度」が得られればベクトルの類似度を計算したことになります。

内積では同じ位置の要素の掛け算を行い、それぞれの要素ごとに和を計算することでベクトルの類似度を計算します。図の例では具体的に下記のような計算を行います。
・左のベクトルと真ん中のベクトルの内積
$$
\large
\begin{align}
1 \times 0 + 0 \times 1 = 0
\end{align}
$$

・左のベクトルと右のベクトルの内積
$$
\large
\begin{align}
1 \times 0.7 + 0 \times 0.7 = 0.7
\end{align}
$$

・真ん中のベクトルと右のベクトルの内積
$$
\large
\begin{align}
0 \times 0.7 + 1 \times 0.7 = 0.7
\end{align}
$$

上記のような計算により、ベクトルの類似度を計算することができます。ここでは単純化にあたってベクトルの要素が$2$つかつ大きさが概ね等しい場合を取り扱いましたが、Transformerでも概ね同じようなイメージで理解することができます。

赤で囲った「統計」と「教科書」は同じ場所が$1$であることが多く類似度が高く(内積が大きい値で)計算される。同様に青で囲った「本屋」と「教科書」も同じ場所が$1$であることが多く類似度が高く計算される。

上記に基づいて類似度の高い単語を線で結ぶことで下図のようなグラフを得ることができます。

Attentionとグラフニューラルネットワーク

$2017$年のTransformerの論文では「RNNを用いないでAttentionのみを用いた」などと記載されており、多くのTransformerの解説コンテンツはこの記載に基づいてAttentionの解説に注力しがちです。

一方で、入門書ではMLP・CNN・RNNを学ぶことが多く、Transformerの解説で唐突にAttentionが出てくることで理解が難しいというのも起こりがちだと思います。実際問題、Attentionはどのように理解すると良いのでしょうか。

上記については「RNN」$\longrightarrow$「GNN(グラフニューラルネットワーク)」$\longrightarrow$「Attention・Transformer」の順に拡張したと解釈すると理解しやすいと思います。

上図は前節では取り扱ったRNNとグラフニューラルネットワークの対応を表すのに用いましたが、右のグラフニューラルネットワークにおける線は前項の「Dot Product Attention」で計算した結果に基づきます。

ここまでの内容を元に、「Transformerは類似度計算に基づいてグラフを構築したグラフニューラルネットワークである」と解釈できます。同時に「Attention処理はグラフニューラルネットワークにおける処理プロセスの$1$つである」と解釈することもできます。

「Attention・Transformer」と「グラフニューラルネットワーク」の対応は上図のように表すことができます。詳細については下記で取り扱ったので省略しますが、Attentionはグラフニューラルネットワークの処理プロセスの$1$つと見なせることは抑えておくと良いです。

Inductive biasとTransformer

当記事ではCNN、RNN、GNNなど、様々なニューラルネットワークを紹介しましたが、これらを理解する上でInductive biasという用語を抑えておくと良いです。

Inductive biasは「ニューラルネットワークを構築する際に設定する構造的仮定」のことを指します。具体的には「画像」は「ピクセルの値が近傍のピクセルと相関する」、「時系列問題」は「近い時点の入力が相関する」などが対応します。

同様にオリジナルのTransformerが取り組んだ「機械翻訳」では、「近い語順同士で相関する」と同時に「似た意味の単語が相関する」ということが起こり得ます。RNNではこの単語の意味まで取り扱えていなかった一方で、Transformerでは取り扱えるようになったというのがブレークスルーが生じた一因になったと解釈できます。また、Transformerでは「近い語順」を取り扱うにあたって、「Positional Encoding」という仕組みを用いることも合わせて抑えておくと良いです。このようにニューラルネットワークを構築する際には「問題の特性」をうまくニューラルネットワークの構造に反映させることが重要になります。

まとめ

Q&A

・グラフが唐突に出てきて難しい
当記事で出てきた「グラフ」は「グラフ理論」という分野の一部です。グラフ理論を詳しく学ぶと難しい一方で、当記事で出てきたレベルでは「グラフ」と「隣接行列」の対応を具体例を元に抑えておくだけで十分です。「行列」と「図」の対応を何度も確認することで理解できるのではないかと思います。

・Transformerが強力な理由
Attentionという仕組みに基づいて位置的には離れているが意味的に重要な関連性を取り扱うことができるようになったことが大きいと解釈できます。

・元論文を読むのに必要な数学レベル
Transformerの元論文を読むにあたっては「行列」について抑えている必要があります。「行列」を無理なく習得するにあたっては高校数学+線形代数の基本は抑えておくのが良いと思います。

さらなる学習にあたって

Udemyで解説コンテンツを作成いたしましたので、下記などを元に確認すると良いと思います。多少難しい内容も含まれますが、映像での解説なので理解しやすいのではないかと思います。

部分分数分解(partial fraction decomposition)の解法まとめ

部分分数分解(partial fraction decomposition)は積分を行う際などによく用いられる演算です。通分による分数の計算の逆演算であると解釈することもできます。当記事では部分分数分解のフォーマルな解法と簡単な式の暗算のやり方について取りまとめを行いました。

・数学まとめ
https://www.hello-statisticians.com/math_basic

部分分数分解

概要

部分分数分解は分母が積の形で表される分数を分解する一連の流れを表す。部分分数分解では具体的には下記のような計算を行う。
$$
\large
\begin{align}
\frac{1}{(x+1)(x+2)} = \frac{1}{x+1} – \frac{1}{x+2}
\end{align}
$$

上記のように部分分数分解は通分による計算の逆演算であると解釈することもできる。

簡易的な解法

$\displaystyle \frac{1}{(x+s)(x+t)}, \, s \neq t$のような分数の場合、下記のように部分分数分解を行うことができる。
$$
\large
\begin{align}
\frac{1}{(x+s)(x+t)} = \frac{1}{t-s} \left[ \frac{1}{x+s} – \frac{1}{x+t} \right]
\end{align}
$$

たとえば$\displaystyle \frac{1}{(x-1)(x+2)}$は下記のように部分分数分解を行える。
$$
\large
\begin{align}
\frac{1}{(x-1)(x+2)} &= \frac{1}{2-(-1)} \left[ \frac{1}{x+1} – \frac{1}{x+2} \right] \\
&= \frac{1}{3} \left[ \frac{1}{x+1} – \frac{1}{x+2} \right]
\end{align}
$$

フォーマルな解法

前項の解法は分母が$2$つの要素の式である場合は概ね有効である一方で、$\displaystyle \frac{1}{x(x+1)(x+2)}$のように$3$つの要素の分解の場合などに用いるフォーマルな解法も抑えておくと良い。
$$
\large
\begin{align}
\frac{1}{x(x+1)(x+2)} = \frac{a}{x} + \frac{b}{x+1} + \frac{c}{x+2} \quad (1)
\end{align}
$$

フォーマルに部分分数分解を行う場合は$(1)$式のように分解を行うことができると仮定し、両辺の対応から$a, b, c$の値を得る。$(1)$式の右辺は下記のように変形できる。
$$
\large
\begin{align}
\frac{a}{x} + \frac{b}{x+1} + \frac{c}{x+2} &= \frac{a(x+1)(x+2)}{x(x+1)(x+2)} + \frac{bx(x+2)}{x(x+1)(x+2)} + \frac{cx(x+1)}{x(x+1)(x+2)} \\
&= \frac{(a+b+c)x^{2} + (3a+2b+c)x + 2a}{x(x+1)(x+2)}
\end{align}
$$

上記と$(1)$式より下記のような連立方程式が得られる。
$$
\large
\begin{align}
a+b+c &= 0 \\
3a+2b+c &= 0 \\
2a &= 2
\end{align}
$$

上記より、$a=1, b=-2, c=1$が得られるので、下記のような部分分数分解が得られる。
$$
\large
\begin{align}
\frac{2}{x(x+1)(x+2)} = \frac{1}{x} – \frac{2}{x+1} + \frac{1}{x+2}
\end{align}
$$

部分分数分解の応用例

定積分の計算

部分分数分解は主に積分を計算する際によく用いられる。たとえば下記のように定積分を計算できる。
$$
\large
\begin{align}
\int_{0}^{1} \frac{1}{(x+1)(x+2)} dx &= \int_{1}^{2} \left[ \frac{1}{x+1} – \frac{1}{x+2} \right] dx \\
&= \left[ \log{|x+1|} – \log{|x+2|} \right]_{0}^{1} \\
&= (\log{2} – \log{3})-(\log{1} – \log{2}) = 2\log{2} – \log{3}
\end{align}
$$

活性化関数(activation function)のグラフの図示

DeepLearningの順伝播の計算では隠れ層におけるそれぞれのニューロンでなんらかの判断が行われるように非線形関数である活性化関数(activation function)を用います。当記事では活性化関数のいくつかの例の数式の確認とグラフの図示を行いました。

・用語/公式解説
https://www.hello-statisticians.com/explain-terms

活性化関数

活性化関数の概要

活性化関数の数式

・シグモイド関数
$$
\large
\begin{align}
y = \frac{1}{1+e^{-x}} = \frac{e^{x}}{e^{x}+1}
\end{align}
$$

・ソフトサイン関数
$$
\large
\begin{align}
y = \frac{1}{1+e^{-x}} = \frac{x}{1+|x|}
\end{align}
$$

・ソフトプラス関数
$$
\large
\begin{align}
y = \log{(1+e^{x})}
\end{align}
$$

・ReLU(ランプ関数)
$$
\large
\begin{align}
y = \max{(0,x)}
\end{align}
$$

・双曲線正接関数
$$
\large
\begin{align}
y = \tanh{x} = \frac{e^{x}-e^{-x}}{e^{x}+e^{-x}}
\end{align}
$$

活性化関数のグラフ

シグモイド関数

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme()

x = np.arange(-5., 5.01, 0.01)
y_1 = 1./(1.+np.e**(-x))

plt.plot(x, y_1)
plt.show()

・実行結果

ソフトサイン関数

y_2 = x/(1+np.abs(x))

plt.plot(x, y_2)
plt.show()

・実行結果

ソフトプラス関数

y_3 = np.log(1+np.e**x)

plt.plot(x, y_3)
plt.show()

・実行結果

ReLU(ランプ関数)

x_ = np.zeros([x.shape[0],2])
x_[:,1] = x
y_4 = np.max(x_, axis=1)

plt.plot(x, y_4)
plt.show()

・実行結果

双曲線正接関数(hyperbolic tangent function)

y_5 = (np.e**x - np.e**(-x)) / (np.e**x + np.e**(-x))

plt.plot(x, y_5)
plt.show()

・実行結果

グラフまとめ

plt.plot(x, y_1, label="sigmoid")
plt.plot(x, y_2, label="softsign")
plt.plot(x, y_3, label="softplus")
plt.plot(x, y_4, label="ReLU")
plt.plot(x, y_5, label="tanh")

plt.legend()
plt.show()

・実行結果