活性化関数の内部状態

活性化関数と内部について

ニューラルネットワークは多くのパラメータを持っており、非線形の問題を解くためには活性化関数が必要です。

本チュートリアルではニューラルネットワークの中身と活性化関数の効果について説明します。大きく分けて2種類の活性化関数があり、一つは出力層用活性化関数と、もう一つは隠れ層用活性化関数に分けられます。

分類問題の場合は、出力層用活性化関数が主に使用されます。例えばA、B、Cというクラスがあり、ある特定データをいずれかのクラスに分類したい時に使用します。このような場合、どのクラスに属するかを分類しなければならず、このような問題においては確率変数を用いることが妥当です。そこで、活性化関数を出力層の前に用いることで出力ユニットの値を確率として解釈することができます。このような場合は主に出力層用活性化関数として、Sigmoid関数とSoftmax関数があります。

非線形問題(分類問題)の場合は、隠れ層用活性化関数を使用します。代表的な活性化関数として、ReLUがあります。

今回は主にこちらの中間層に活性化関数を用いるときの役割についてXOR問題という非線形分類問題を用いて可視化していきたいと思います。

必要なライブラリ

In [1]:
import numpy as np
import renom as rm
from renom.optimizer import Sgd
import matplotlib.pyplot as plt

XORデータの作成

XORデータとは以下のように4つの点を2つのクラスに分類する問題となっています。

この問題はよく非線形分離の問題に使われています。
今回はこのXOR問題を用いてRelu活性化関数の役割について見ていきたいと考えています。
In [2]:
X = np.array([[1,1],
            [1,0],
            [0,1],
            [0,0]])

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

モデル定義

In [3]:
class Mnist(rm.Model):
    def __init__(self):
        self.layer1 = rm.Dense(output_size=5)
        self.layer2 = rm.Dense(1)

    def forward(self, x):
        t1 = self.layer1(x)
        t2 = rm.relu(t1)
        t3 = self.layer2(t2)
        return t3

今回は出力は以下のように計算されます。

\begin{equation*} h1 = w^{i→h}_{11} \times x_1 + w^{i→h}_{21} \times x_2 + b^{i→h}_{1} \end{equation*}
\begin{equation*} h2 = w^{i→h}_{12} \times x_1 + w^{i→h}_{22} \times x_2 + b^{i→h}_{2} \end{equation*}
\begin{equation*} h3 = w^{i→h}_{13} \times x_1 + w^{i→h}_{23} \times x_2 + b^{i→h}_{3} \end{equation*}
\begin{equation*} h4 = w^{i→h}_{14} \times x_1 + w^{i→h}_{24} \times x_2 + b^{i→h}_{4} \end{equation*}
\begin{equation*} h5 = w^{i→h}_{15} \times x_1 + w^{i→h}_{25} \times x_2 + b^{i→h}_{5} \end{equation*}
\begin{equation*} output = w^{h→o}_{1} \times Relu(h1) + \end{equation*}
\begin{equation*} w^{h→o}_{2} \times Relu(h2) + \end{equation*}
\begin{equation*} w^{h→o}_{3} \times Relu(h3) + \end{equation*}
\begin{equation*} w^{h→o}_{4} \times Relu(h4) + \end{equation*}
\begin{equation*} w^{h→o}_{5} \times Relu(h5) + b^{h→o} \end{equation*}

学習

In [4]:
epoch = 50
batch = 1
N = len(X)
optimizer = Sgd(lr=0.1, momentum=0.4)

network = Mnist()
learning_curve = []

for i in range(epoch):
    perm = np.random.permutation(N)
    loss = 0
    for j in range(0, N // batch):
        train_batch = X[perm[j*batch : (j+1)*batch]]
        response_batch = y[perm[j*batch : (j+1)*batch]]
        with network.train():
            result = network(train_batch)
            l = rm.sigmoid_cross_entropy(result, response_batch)
        grad = l.grad()
        grad.update(optimizer)
        loss += l
    train_loss = loss / (N // batch)
    learning_curve.append(train_loss)
plt.plot(learning_curve, linewidth=3, label="train")
plt.show()
../../../_images/notebooks_basic_algorithm_activation_notebook_9_0.png
In [5]:
print("[0, 0]:{}".format(network([0,0]).as_ndarray()))
print("[1, 1]:{}".format(network([1,1]).as_ndarray()))
print("[1, 0]:{}".format(network([1,0]).as_ndarray()))
print("[0, 1]:{}".format(network([0,1]).as_ndarray()))
[0, 0]:[[1.962966]]
[1, 1]:[[1.962966]]
[1, 0]:[[-2.961516]]
[0, 1]:[[-3.0228367]]

見て分かるようにXOR問題に対して良い分類結果が得られました。0を閾値として用いることで[1,1],[0,0]のデータと[1,0],[0,1]のデータを分割することができそうです。ところでこの出力値は活性化関数の重み付けをすることで計算されます。-3から3までの入力値をx1とx2の2つの入力ユニットとして想定し、出力がどんな表面をしているかを見たいと思います。高さが出力値に対応しています。

\begin{equation*} output = w^{h→o}_{1} \times Relu(h1) + \end{equation*}
\begin{equation*} w^{h→o}_{2} \times Relu(h2) + \end{equation*}
\begin{equation*} w^{h→o}_{3} \times Relu(h3) + \end{equation*}
\begin{equation*} w^{h→o}_{4} \times Relu(h4) + \end{equation*}
\begin{equation*} w^{h→o}_{5} \times Relu(h5) + b^{h→o} \end{equation*}

出力は5つの活性化関数の重み付け和から構成されます。

非線形の活性化関数を用いることで非線形の分類問題を解くことができましたが、XOR分類問題をReluという活性関数を使って解いた出力の表面はx1とx2という二つの入力ユニットを-3から3までの場合で見たとき、上の図のようになります。次に、出力値を構成する5つのReluの出力がどのようになっているかを見たいと思います。

上の図は以下の数式を元に計算される値です。

\begin{equation*} output = w^{h→o}_{1} \times Relu(h1) \end{equation*}
\begin{equation*} h1 = w^{i→h}_{11} \times x_1 + w^{i→h}_{21} \times x_2 + b^{i→h}_{1} \end{equation*}

上の図は以下の数式を元に計算される値です。

\begin{equation*} output = w^{h→o}_{2} \times Relu(h2) \end{equation*}
\begin{equation*} h2 = w^{i→h}_{12} \times x_1 + w^{i→h}_{22} \times x_2 + b^{i→h}_{1} \end{equation*}

重みの可視化

更にRelu活性化を使った場合の重みについても見ていきたいと思います。

中身について

出力層への重みは順に-2.2, 1.1, -1.2, -0.2, -1.8となっております。

クラス1が0以上でクラス0が0以下の値を出力ユニットで取りたいとすると、二番目の重みのみを使うことができればプラスの値に設定できそうですが、他の値を使ってしまうとマイナスに振れてしまいそうです。今回は(0,0)と(1,1)という2点をクラス1、 (1,0)と(0,1)をクラス0に分類してほしいです。従って、(0,0)と(1,1)が入力として与えられたときに2番目の隠れ層から出力層への重みが一番強く効いてほしいです。まず、(0,0)について考えると、入力に(0,0) が与えられたとき、重みは特に計算されないため、二番目の隠れ層ユニットのバイアスを一番大きく、他のバイアスを0にして問題ありません。次に(1,1)の場合ですが、1番目と5番目の出力層への重みは大きく、マイナスへ計算されてしまう可能性があるので、2番目の隠れ層ユニットだけ大きく、他のユニットは計算されないように0になってくれると嬉しいです。

そのような設定になっていれば(0,0)と同様に(1,1)もクラス1へ計算することが可能となります。入力(0,1)(1,0) は隠れ層から出力層への重みが大きな負の値を取っています。正の重みに対応するユニットが0出力で、(1,0)と(0,1)の点がクラス0として計算されます。

ReLUのスパース効果

ところで、先ほどからクラス1に計算を行いたい場合には出力値が0より大きくなり、クラス0に計算を行いたい場合には出力値が0以下になるように計算を行いたいとしました。正の値、または負の値にしたいときに使うノードが異なりますが、これを実現できるのがReluのスパース化によるものです。

例えば2番目の隠れ層ユニットは正の出力に計算を持っていきたい場合に使えそうです。また、それ以外の大きな負の値をとる1番目のユニットや5番目のユニットに関しては負の出力に計算を持っていく場合に有効ではないかと思います。

Relu関数についてはご存知の方も多いように0以下の値は全て0となりますので、(1,0)や(0,1)が入力として与えられたときには1番目のノードや5番目のノードで大きな正の出力を行えるようにすることでマイナスの値にすることができ、他の2番目や3番目などのノードについてはマイナスに数を振っておくことで計算に使わないノードとすることができます。更に(1,1)が入力のときにはこれらの(1,0)や(0,1)で使っていた重みの値が打ち消されるように他の重みを決定することと、2番目のノードに向かう出力のみが大きくなるように設定をすることで、上記のような計算が可能となっています。具体的な計算については実際の重みの値は上の図にある通りですので、(0,1)の場合などにどのように出力が計算されるのかを見ることをお勧めします。

まとめ

ニューラルネットワークはこのように単純な非線形の活性化関数を用いて非線形も分類問題を解くことが可能になります。 活性化関数が無い場合についてはXOR問題のような非線形の分類問題は出力の表面がReluを使った場合のように複雑な形状ではなく、直線や平面の組み合わせでしか表現できませんので、解くことのできない問題が出てきます。 活性関数の役割を知ることで例えば敢えて出力を線形関数の重み付け和で算出したい場合や、新しいモデルを作成していく場合において、そのモデルにおいて活性化関数の役割を理解して構築することでよりニューラルネットワークを完全なブラックボックスとして用いるのではなく、役割を理解して、結果を理解することができると考えています。例えば画像の分類モデルなどにおいてはVGG16のような著名なモデルが既にネットワーク構造、活性化関数で何を使うかはだいたい決まっているために、精度を上げるためのノウハウというよりは,このチュートリアルが活性化関数をなぜ使うのかの理解への一つの道標になれば幸いです。