72
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

全結合のNNで層を深く(ディープに)することの意味

Posted at

全結合のNNで層を深く(ディープに)することの意味

畳み込みではない、層間が全結合の順伝播型ニューラルネットワークにおいて、層を深く(ディープに)することには、どんな意味があるのでしょう。

何周遅れの話なんだという感じですが、自分でネットワークを実装してみて腹落ちしたような気がするので、投稿します。

ニューラルネットワークの動機

そもそも、コンピュータプログラムは、入力xを受け取り、出力yを返す関数fとして理解できます。なお、数学的な意味での関数と異なるのは、入出力等の副作用(side effect)を伴うところです。

入出力

プログラマは、小さな関数を重ね合わせたり(関数型言語)、手続きを並べたり(手続き型言語)、オブジェクト同士でメッセージのやり取りをしたり(オブジェクト指向言語)して、関数fを実装します。

ところが、何らかの理由で、関数fを直接書き下せないことがあります。一例を挙げれば、囲碁の局面を評価する評価関数。囲碁は、自然言語では「このへんの白は模様が良い」「黒の右辺には厚みがある」などと語られますが、これを明示的にプログラミングするのは非常に難しい。

ここに、関数fを直接書き下すのでなく、何らかの手段で近似したい、という動機が生まれるわけです。これがニューラルネットワークを生み出した動機だと、私は思っています。——歴史を調べたわけではないので、間違っているかもしれませんが…。

ユニットの組み合わせ

さて、ニューラルネットワークでは、簡単な関数(活性化関数)からなるユニットを複数組み合わせることで、複雑な関数を近似します。

活性化関数には、例えば、シグモイド関数というものがあります。以下にシグモイド関数を描画します。(最初は主流のReLUでやってみたけど、あまり面白くなかったので、シグモイドでいきます。)

import numpy as np
import matplotlib.pyplot as plt


def visualize(xs, ys):
    plt.figure()
    plt.plot(xs, ys, marker='.')
    plt.grid()
    plt.xlabel('x')
    plt.ylabel('y')
    plt.show()


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

    def identity(x):
        return x

    @classmethod
    def rand(cls, np_random):
        return np_random.rand() * 10 - 5
x = np.arange(-10, 10, 0.01)
y = B.sigmoid(x)
visualize(x, y)

output_4_0.png

シグモイド関数は、ただこれだけの素片ですが、入力のxに重みとバイアスを加味することで、様々に変形します。以下に変形例を二点描画します。

x = np.arange(-10, 10, 0.01)
y = B.sigmoid(10 * x + 100)
visualize(x, y)

output_6_0.png

x = np.arange(-10, 10, 0.01)
y = B.sigmoid(-0.3 * x - 5)
visualize(x, y)

output_7_0.png

こうした、シンプルながらも表情豊かな曲線を合成することで、より複雑な曲線を近似できる、というのがニューラルネットワークの基本思想だと私は理解しています。

ネットワークの作成

さて、ネットワークと呼ぶくらいなので、ユニットを縦横無尽に組み合わせます。

以下に、全結合層からなるネットワークの例を図示します。矢印が密集(Dense)しているのが、なんだか気持ち悪いですね。

全結合ネットワーク

この、(中間層ごとの)ユニット数、中間層の数、共に3のネットワークを作成してみることにしましょう。重みやバイアスは適当に(シード固定の乱数を使って)設定することにします。

まずは、色々と条件を変えて実験できるように、汎用的な形で全結合層とネットワークのクラスを書きます。入力Xはベクトルを想定しているので、行列の演算が出てきますが、難しいものではないかと思います。

class Dense:
    def __init__(self, n, input_dim, activation, np_random):
        self.n = n
        self.input_dim = input_dim
        self.activation = activation
        self.np_random = np_random
        
        # 重みは、input_dim * n の行列となる
        weights = [[B.rand(np_random) for _ in range(self.n)] for _ in range(self.input_dim)]
        self.weights = np.array(weights)
        
        # バイアスは、1 * n のベクトルとなる
        self.bias = np.array([B.rand(np_random) for _ in range(self.n)])
    
    def forward(self, X):
        assert X.size == self.input_dim
        
        # 入力X(1 * input_dim のベクトル)に重みをかけて、バイアスを足す
        self.forwarded = self.activation(np.dot(X, self.weights) + self.bias)
        return self.forwarded.copy()


class Network:
    def __init__(self, np_random):
        self.layers = []
        self.np_random = np_random
        
    def add(self, n, input_dim=2, activation=None):
        _input_dim = input_dim
        if len(self.layers) > 0:
            _input_dim = self.layers[-1].n
        
        if activation is None:
            _activation = B.identity
        elif activation == 'sigmoid':
            _activation = B.sigmoid
        elif activation == 'identity':
            _activation = B.identity
        else:
            raise ValueError('Unknown activation: {}'.format(activation))
        
        self.layers.append(Dense(n, _input_dim, _activation, np_random))
    
    def forward(self, X):
        self.forwarded = X
        for layer in self.layers:
            self.forwarded = layer.forward(self.forwarded)
        return self.forwarded.copy()


xs = [np.array([x]) for x in np.arange(-5, 5, 0.01)]

さて、適当なネットワークを作成し、身長と体重風の数値(私のではない)を通してみましょう。

np_random = np.random.RandomState(0)
n = Network(np_random)
n.add(3, input_dim=2, activation='sigmoid')
n.add(3, activation='sigmoid')
n.add(1)

x = np.array([165, 52])
y = n.forward(x)

print(y)
[-1.02875705]

なんぞ、よくわからん数値が出てきました。とりあえず、動いているようです。こいつ・・・動くぞ!

それでは、ユニット数、中間層の数、共に3のネットワークを作成してみましょう。

# 実験0
np_random = np.random.RandomState(0)
n0 = Network(np_random)
n0.add(input_dim=1, n=3, activation='sigmoid')   # 入力層
n0.add(input_dim=3, n=3, activation='sigmoid')   # 中間層1
n0.add(input_dim=3, n=3, activation='sigmoid')   # 中間層2
n0.add(input_dim=3, n=3, activation='sigmoid')   # 中間層3
n0.add(input_dim=3, n=1, activation='identity')  # 出力層
ys0 = [n0.forward(x) for x in xs]
visualize(xs, ys0)

output_14_0.png

うん。元のシグモイド関数の面影は消えましたね。

このように、ユニットを組み合わせてネットワークを作成することで、任意の関数を近似(表現)できそうな気がします。あくまで気分ですけどね。

本題:ネットワーク対決

さて、かねがね疑問に思っていたのですが、1)中間層が1つしかないがユニット数の多いネットワークと、2)ユニット数は少なめだが中間層の多い——よりディープな——ネットワークでは、どちらが表現力に富むのでしょうか。

以下、対決させてみたいと思います。総ユニット数(シグモイド関数の適用回数)は256で固定します。重みやバイアスを変えつつ、グラフを10個ずつ描画します。

1)中間層が1つしかないがユニット数の多いネットワーク

# 実験1
for seed in range(10):
    np_random = np.random.RandomState(seed)
    n1 = Network(np_random)
    n1.add(256, input_dim=1, activation='sigmoid')
    n1.add(1)
    ys1 = [n1.forward(x) for x in xs]
    visualize(xs, ys1)

output_18_0.png

output_18_1.png

output_18_2.png

output_18_3.png

output_18_4.png

output_18_5.png

output_18_6.png

output_18_7.png

output_18_8.png

output_18_9.png

ウネウネしたグラフが10個描画できました。

2)ユニット数は少なめだが中間層の多いネットワーク

# 実験2
for seed in range(10):
    np_random = np.random.RandomState(seed)
    n2 = Network(np_random)
    n2.add(16, input_dim=1, activation='sigmoid')  # 1
    n2.add(16, activation='sigmoid')  # 2
    n2.add(16, activation='sigmoid')  # 3
    n2.add(16, activation='sigmoid')  # 4
    n2.add(16, activation='sigmoid')  # 5
    n2.add(16, activation='sigmoid')  # 6
    n2.add(16, activation='sigmoid')  # 7
    n2.add(16, activation='sigmoid')  # 8
    n2.add(16, activation='sigmoid')  # 9
    n2.add(16, activation='sigmoid')  # 10
    n2.add(16, activation='sigmoid')  # 11
    n2.add(16, activation='sigmoid')  # 12
    n2.add(16, activation='sigmoid')  # 13
    n2.add(16, activation='sigmoid')  # 14
    n2.add(16, activation='sigmoid')  # 15
    n2.add(16, activation='sigmoid')  # 16, 16*16=256
    n2.add(1)
    ys2 = [n2.forward(x) for x in xs]
    visualize(xs, ys2)

output_21_0.png

output_21_1.png

output_21_2.png

output_21_3.png

output_21_4.png

output_21_5.png

output_21_6.png

output_21_7.png

output_21_8.png

output_21_9.png

不気味なグラフが10個描画できました。

ジャッジ

定性的な判断となってしまいますが、1のネットワークよりも、2のネットワークのほうが、表現力が豊かなように思います。

表現力の違いを生み出す要因としては、1よりも2のほうが重み(つまりパラメータ)の数が多いことが一因と考えられます。1の重みの数は512、2の重みの数は3616(16 + 14 * 256 + 16)です。ネットワークを作成した人間の言うことですので、計算については間違いないと思います。あるいは、図のイメージで言えば、後者のネットワークは、矢印の密集っぷりが半端ないということです。

そのパラメータの多さが、ネットワークに表現力を与えているのでしょう。もちろん適切にパラメータをチューニングできなければ、ただの抽象画で終わってしまうわけですが…。

3)重みの数が1と同程度のネットワーク

検証のため、重みの数が1と同程度(510)のネットワークを作成し、描画してみます。

# 実験3
for seed in range(10):
    np_random = np.random.RandomState(seed)
    n3 = Network(np_random)
    n3.add(15, input_dim=1, activation='sigmoid')
    n3.add(16, activation='sigmoid')
    n3.add(15, activation='sigmoid')
    n3.add(1)
    ys3 = [n3.forward(x) for x in xs]
    visualize(xs, ys3)

output_25_0.png

output_25_1.png

output_25_2.png

output_25_3.png

output_25_4.png

output_25_5.png

output_25_6.png

output_25_7.png

output_25_8.png

output_25_9.png

定性的な判断となってしまいますが、1の表現力に近づいた気がします。

結論

畳み込みではない、層間が全結合の順伝播型ニューラルネットワークにおいて、層を深く(ディープに)することには、パラメータの数を増やし、ネットワークの表現力を増やす意味がある。ただし、適切なチューニングを行えなければ、表現力を持て余して意味がない。

余談

逆に、層を深くしすぎて、表現力を失うということもあります。

# 実験4
np_random = np.random.RandomState(seed)
n4 = Network(np_random)
n4.add(10, input_dim=1, activation='sigmoid')
for i in range(1):
    n4.add(10, activation='sigmoid')
n4.add(1)
ys4 = [n4.forward(x) for x in xs]
visualize(xs, ys4)

output_29_0.png

# 実験5
np_random = np.random.RandomState(seed)
n5 = Network(np_random)
n5.add(10, input_dim=1, activation='sigmoid')
for i in range(100):
    n5.add(10, activation='sigmoid')
n5.add(1)
ys5 = [n5.forward(x) for x in xs]
visualize(xs, ys5)

output_30_0.png

これは、シグモイド関数の適用を重ねることで、不動点(の定数倍)へと収束してしまったものと思われます。

xs6 = np.arange(-100, 100)
ys6 = np.copy(xs6)
for i in range(30):
    ys6 = B.sigmoid(ys6)
visualize(xs6, ys6)
print('sigmoid(0.6590460684074066) =', B.sigmoid(0.6590460684074066))

output_32_0.png

sigmoid(0.6590460684074066) = 0.6590460684074066

層を深くすれば、パラメータも膨大になるし、結局のところ、適度なバランスが必要といったところでしょうね。なんと、ありふれた結論…。

72
62
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
62

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?